Complete macOS app for MPF: signed universal .app with auto-signed dumping tools#977
Complete macOS app for MPF: signed universal .app with auto-signed dumping tools#977knutwurst wants to merge 48 commits into
Conversation
Copy all 191 English strings from MPF.UI/Resources/Strings.xaml into MPF.UI.Avalonia/Resources/Strings.axaml (Avalonia ResourceDictionary, assembly=System.Runtime) and merge it into App.axaml via ResourceInclude.
…services Ports Theme, Constants (DoubleCollection -> List<double>), ElementConverter, MessageBoxWindow, DialogService (sync/async bridge), and StorageDialogs from MPF.UI to MPF.UI.Avalonia; build passes with 0 warnings.
Add Brushes.axaml with 30 SolidColorBrush resource-key defaults (sourced from MPF.UI/App.xaml where present, sensible light-theme defaults for the 6 WPF SystemColors keys), and Controls.axaml with HeaderedContentControl .groupbox and Button.custom styles. Both are wired into App.axaml.
Adds MPF.UI.Avalonia/Controls/LogOutput.axaml + .axaml.cs, translating the WPF RichTextBox/FlowDocument log panel to an Avalonia SelectableTextBlock with inline Run coloring inside a ScrollViewer. Preserves the public EnqueueLog(LogLevel, string) API for wiring to MainViewModel.Init in Task 8.
Adds UserInput.axaml/.cs with full StyledProperty parity to the WPF DependencyProperty API, plus StringToDoubleConverter to bridge the string TextHeight property to Avalonia's double Height.
Port the WPF MainWindow + WindowBase to Avalonia: settings/controls/ status group boxes, custom title-bar chrome, menu, and log output, wired to MainViewModel.Init with the DialogService message-box bridge and a ShowMediaInformationWindow stub. Secondary windows (Options/CheckDump/ CreateIRD) are stubbed pending later tasks. Also fix LogOutput so its named controls resolve when loaded via AvaloniaXamlLoader.
…nia has no IsCheckable)
- Add OptionsWindow.axaml with all five tabs (General, Paths, Dumping, Programs, Login Info) including every GroupBox/CheckBox/ComboBox/Slider from the WPF original; uses x:CompileBindings="False" for deep VM paths. - Add OptionsWindow.axaml.cs: parameterless ctor for XAML compiler + real ctor(Options); Browse buttons via StorageDialogs.OpenFileAsync / PickFolderAsync; Accept/Cancel set SavedSettings; Redump login test calls RedumpClient.ValidateCredentials and shows result via MessageBoxWindow; Slider.Ticks populated in code-behind as AvaloniaList<double> (Avalonia does not accept List<double> in XAML binding for that property). - Replace ShowOptionsWindow stub in MainWindow.axaml.cs with modal ShowDialog + inline OnOptionsUpdated: refreshes output path, applies language change, calls MainViewModel.UpdateOptions, re-applies theme and media-type visibility.
…nter/Escape on Options buttons
…ndings (Avalonia lacks WPF enum-cast indexer syntax)
- Add CheckDumpWindow.axaml: custom-chrome window with Settings group (input path + Browse, system combo, program combo), Status group, warning label, and Check Dump / Cancel buttons; inherits WindowBase. - Add CheckDumpWindow.axaml.cs: ctor sets DataContext to CheckDumpViewModel; BrowseFile uses StorageDialogs.OpenFileAsync; SelectionChanged handlers for system/program/input are guarded by CanExecuteSelectionChanged; CheckDump runs on a background thread via Task.Run to avoid UI-thread deadlock; ShowMediaInformationWindow bridges the synchronous ProcessUserInfoDelegate to MediaInformationWindow via Dispatcher.UIThread.InvokeAsync. - Replace ShowCheckDumpWindow stub in MainWindow with hide/show UX matching WPF.
Translates MPF.UI/Windows/CreateIRDWindow.xaml(.cs) to Avalonia: - CreateIRDWindow.axaml: custom chrome title bar, Input group (ISO path + .getkey.log path with Browse buttons), Key/DiscID/PIC Expanders, Status group, Create IRD / Cancel buttons; all *Enabled bindings wired. - CreateIRDWindow.axaml.cs: ctor sets DataContext = new CreateIRDViewModel(); five async Browse helpers (ISO, Log, Key, PIC open-pickers; IRD save-picker) using StorageDialogs; TextChanged/Browse Click handlers all guarded by CanExecuteSelectionChanged; OnCreateIRDClick collapses expanders, disables UI, prompts for output path via BrowseOutputFile, runs synchronous CreateIRDViewModel.CreateIRD(outputPath) on a background thread via Task.Run, then shows success (YesNo → reset or close) or failure (Ok) MessageBoxWindow. - MainWindow.axaml.cs: replaces the "coming soon" async stub with the real ShowCreateIRDWindow() (Hide → new CreateIRDWindow(this) → Closed → Show + Activate), matching WPF UX.
…de button Ports the WPF RingCodeGuideWindow to Avalonia: custom title-bar with minimize/close Path-buttons (using WindowBase handlers), two-tab layout showing annotated ring-code guide images (1-layer and 2-layer), and all explanatory Run/TextBlock text. Images bundled as AvaloniaResource (avares://MPF.Avalonia/Images/). MediaInformationWindow.OnRingCodeGuideClick stub replaced with a non-async handler that opens the real window via win.Show(this).
…ming Add DarkModeTheme, LightModeTheme, and CustomTheme under MPF.UI.Avalonia/Themes/ and replace the ApplyTheme() no-op in MainWindow with the same option-based selection logic as the WPF source; also sets RequestedThemeVariant so Fluent controls follow dark/light mode.
…k-mode OSes App used RequestedThemeVariant=Default which follows the OS. On a dark-mode host, FluentTheme rendered control text white while the custom theme brushes hardcode a light (white) window background, making checkboxes, tab headers and buttons invisible. MPF's WPF UI is light-by-default with an opt-in dark mode, so pin the variant to Light; ApplyTheme() still switches to Dark when the user enables dark mode (verified: dark path uses dark background + light text).
…ndows-style title bar
Remove SystemDecorations="None", TransparencyLevelHint, and Background="Transparent"
from all 6 windows; unwrap the outer Border; delete the hand-drawn title-bar Grid
(icon Image, centered TextBlock with ContextMenu, Path-based min/close Buttons).
Each window now sets Background="{DynamicResource WindowBrush}" and relies on the
native macOS traffic-light chrome for close/minimize/zoom.
MainWindow keeps its menu bar (File/Tools/Help/Languages + new Window menu with
Minimize and Close items) rendered as a plain <Menu> at the top of the StackPanel.
OptionsWindow title defaults to OptionsTitleString in XAML; the caller may override
it (first-run title) via Window.Title as before. CreateIRDWindow title switched to
the dedicated CreatePS3IRDTitleString resource.
WindowBase.cs is unchanged (TitleMouseDown left in place; CloseButtonClick /
MinimizeButtonClick still used by the Window menu items in MainWindow).
…volume picker, program discovery - Custom buttons now use theme brushes + explicit foreground so labels are legible in dark mode (were white-on-light-grey). - Shrink oversized FluentTheme tab-header font. - Drive picker shows the device/volume path (Name) instead of a Windows drive letter; relabel 'Drive Letter' -> 'Drive' / 'Laufwerk'. - Set working directory to the folder next to the .app bundle at startup so relative 'Programs/<tool>' paths resolve (fixes empty dumping-program list / 'No dumping program found').
…able/untruncated buttons, scrollable resizable main window - Options.DefaultDiscImageCreatorPath: detect macOS via RuntimeInformation (Environment.OSVersion.Platform returns Unix on macOS, so the default wrongly used the Linux 'DiscImageCreator.out' name instead of 'DiscImageCreator'). Conditional-compiled to keep old TFMs building. Also fixes the CLI on macOS. - Reduce app-wide font (ControlContentThemeFontSize + Window FontSize) so button labels stop truncating; shrink oversized tab headers. - MainWindow: wrap content in a ScrollViewer and make the window resizable (was SizeToContent + non-resizable, which pushed the log's Clear/Save buttons off-screen). - LogOutput Clear/Save buttons: use the real 'custom' style class (was 'CustomButton', a no-op).
…uttons visible Dock the top sections, let the log Expander fill the remaining height, and pin the log's Clear/Save buttons to the bottom of the control (DockPanel) so they no longer get clipped off the bottom of the window. Log area background made dark to fill cleanly.
On macOS, strip the download quarantine flag and apply an ad-hoc code signature to every Mach-O file under the sibling Programs/ folder at launch. This lets bundled helpers (redumper, DiscImageCreator, their dylibs) run without the user manually clearing Gatekeeper — and is required on Apple Silicon, which refuses to run unsigned binaries. Best-effort: silently no-ops if xattr/codesign are unavailable.
…so dyld finds their libs
redumper ships its dylibs in a sibling lib/ folder but its only rpath is
@executable_path/../lib, so dyld looked one level too high and the tool was killed
('cannot be opened'). On startup, for each bundled executable that has a sibling lib/
folder, add an @executable_path/lib rpath (then re-sign). Verified: redumper runs.
Note: DiscImageCreator is unaffected here — it is x86_64 and hard-links
/opt/local/lib/libarchive.13.dylib (MacPorts), which must be installed separately.
Publish both osx-arm64 and osx-x64, fuse the native apphost/dylibs with lipo, and ad-hoc sign per-file so one bundle runs natively on Apple Silicon and Intel Macs. Single-arch builds remain available via an explicit RID argument. README updated.
… a clean signature
…imes publish-nix.sh now builds and zips the cross-platform Avalonia UI with the same scheme as the CLI (MPF.UI.Avalonia_<framework>_<runtime>_<config>.zip), bundling the dumping tools under Programs/, for net10.0 across win-x64/win-arm64/linux-x64/linux-arm64/osx-x64/osx-arm64. The Build-and-Test workflow gains workflow_dispatch (manual runs on any branch) and links the new macOS/Linux/Windows Avalonia UI artifacts in the rolling release notes.
…rsal .app Adds a build-macos-app job (macos-latest, needs: build) that runs build-macos-app.sh to produce the lipo'd universal (arm64+x64) ad-hoc-signed .app, zips it, and appends MPF.UI.Avalonia_macos_universal_release.zip to the rolling release (without replacing the other artifacts). build-macos-app.sh now falls back to the PATH dotnet when no repo-local ./.dotnet exists, so it works on CI runners as well as developer machines.
The internal tools (redumper, DiscImageCreator, ...) were launched with UseShellExecute and no redirection, so on Windows they got their own console window and on macOS their output was invisible. Add an optional OutputReceived handler to BaseExecutionContext: when set, stdout/stderr are captured (UseShellExecute=false, CreateNoWindow) and forwarded line-by-line; when null, the legacy console-window behavior is preserved. DumpEnvironment forwards the handler, and MainViewModel routes it to the logger so the program's live output appears in the log panel. Note: when a handler is attached, the tool no longer opens a separate console window.
Read the internal program's stdout/stderr raw on dedicated threads and distinguish carriage-return progress updates (\r) from committed lines (\n) via a ProgramOutputHandler delegate (net20-safe, replaces Action<string>). The Avalonia LogOutput overwrites the current line in place for \r updates so progress (e.g. redumper's percentage) animates on a single line instead of spamming new lines. Capture is opt-in per frontend (MainViewModel. ProgramOutputSink), so Windows/WPF keeps its console window when no sink is attached.
The dumping program's output is now streamed into the in-app log, so the hint to look at a separate console window is no longer accurate.
Use the OS system font (San Francisco on macOS) instead of bundling Inter, switch the Fluent accent to the macOS system blue (#007AFF light / #0A84FF dark), and soften the group boxes and custom buttons (subtle translucent borders, larger corner radius) so the UI no longer looks like a Windows port.
…ut names On Unix/macOS DriveInfo.VolumeLabel is the mount path (e.g. /Volumes/DVD_ROM), which produced output folders like '_Volumes_DVD_ROM'. Use the final path component (e.g. 'DVD_ROM') instead.
….app bundle Settings already load/save via OptionsLoader, but on macOS the 'portable' config was created inside the rebuild-volatile .app bundle and lost on every rebuild. Prefer ~/.config/mpf so settings persist like they do on Windows.
Set the window icon from the existing Icon.ico and bundle an AppIcon.icns (generated from it) in the macOS .app via CFBundleIconFile so the dock/Finder show the MPF icon.
…rst) Add a short author header to the new Avalonia source files and update the csproj authors, About box, and README to credit Knutwurst (https://github.com/knutwurst).
|
There is another PR already opened for this functionality: #975 |
… review) - Skip the @executable_path/lib rpath when already present (no duplicate-rpath error on relaunch) and only re-sign binaries that changed or aren't validly signed yet. - Drain stdout before WaitForExit in the process helper to avoid a pipe-buffer deadlock; add otool/codesign --verify checks via that helper. - Isolate per-file failures in PrepareExternalTools so one bad file doesn't abort the rest. - Skip Java .class files in IsMachO (shared 0xCAFEBABE magic). - LogOutput.SaveInlines: use File.Create (truncate) so re-saving can't leave stale tail bytes. - Set MainViewModel.ProgramOutputSink before Init so any env built during init has the sink.
|
Thanks — and apologies for the overlap, I hadn't seen #975 when I started. You're right that #975 is the primary Avalonia-UI effort and it's further along. Where this PR adds something is the complete macOS app experience — essentially the "icons and niceties on macOS" that #975 mentions deferring to a later PR:
I'd be glad to rebase just these macOS-specific pieces on top of #975 and contribute them there (or coordinate with @whatev-indus / @Deterous) rather than land a parallel UI — whatever means the least duplicate work for you. Happy to align with #975. |
The reason for deferring to a later PR is to minimize the review burden. |
| TotalSize = driveInfo.TotalSize; | ||
| VolumeLabel = driveInfo.VolumeLabel; | ||
|
|
||
| // On Unix/macOS DriveInfo.VolumeLabel is the mount path (e.g. "/Volumes/DVD_ROM"), |
There was a problem hiding this comment.
Actually redumper needs the BSD drive path for dumping, not the mount path
There was a problem hiding this comment.
Good catch. Fixed in 2e4d668.
Added a new Drive.DevicePath property that resolves the BSD device node via diskutil info on macOS, with a fallback to the mount path if diskutil isn't available. Drive.Name stays the mount path so the existing filesystem detection (Directory.Exists, Path.Combine on disc contents) keeps working unchanged. The execution contexts for Aaru, DiscImageCreator and Redumper now get DevicePath instead of Name, so redumper sees /dev/disk2 rather than /Volumes/DVD_ROM.
|
|
||
| # Builds the signed, universal (arm64 + x64) macOS .app on a macOS runner (needs lipo + | ||
| # codesign, which only exist on macOS) and appends it to the same rolling release. | ||
| build-macos-app: |
There was a problem hiding this comment.
Building is preferred in the build script rather than GHA
There was a problem hiding this comment.
Makes sense, moved into the script in 3fee7c3. build-macos-app.sh now also produces MPF.UI.Avalonia_macos_<mode>_release.zip next to the .app. Naming follows what publish-nix.sh uses for the other targets. The macOS job just runs the script and uploads the file.
…path redumper and DiscImageCreator read raw blocks from the device, so they need the BSD path (e.g. /dev/disk2). DriveInfo.Name on macOS is the mount path (/Volumes/DVD_ROM), which the tools can't dump from. Add a Drive.DevicePath property that resolves the BSD device node via `diskutil info` on macOS; on Windows / Linux it stays equal to Name. Hand DevicePath (not Name) to the execution contexts. Name is left as the mount path so filesystem-level detection (Directory.Exists / Path.Combine on disc contents) keeps working unchanged. Falls back to the mount path if diskutil is unavailable.
Keep the packaging logic in one place. The script now emits `MPF.UI.Avalonia_macos_<mode>_release.zip` next to the .app it produces, matching the naming used by publish-nix.sh. The macOS-runner job just runs the script and uploads the artifact.
|
I am going to be placing my review effort on the existing PR with regards to all of the Avalonia UI. I definitely will be interested in the Mac build script and that additional setup as well as any improvements that can be made. I request that you do not make too many changes in your existing PR as I will likely request a stripped-down PR with just the unique functionality from here. |
Complete macOS app for MPF
MPF's GUI is WPF/WinForms and therefore Windows-only. This PR delivers a complete, ready-to-run macOS application: a signed, universal
.appbundle containing the full MPF UI, with native macOS window controls, light/dark themes, and the bundled dumping tools made runnable out of the box. The UI is built with Avalonia and reuses the existingMPF.Frontendview-models unchanged — it's a second View layer over the same MVVM core, not a logic rewrite.Screenshots
The macOS app
.app— one bundle runs natively on Apple Silicon and Intel; built byMPF.UI.Avalonia/build-macos-app.sh.@executable_path/librpath to the bundled tools under a siblingPrograms/folder — soredumper/DiscImageCreatorrun on Apple Silicon with no manual Gatekeeper/codesignsteps.stdout/stderr(incl.redumper's carriage-return progress) is streamed into the in-app log. Opt-in per frontend, so Windows/WPF keeps its console behavior.Full UI parity
LogOutput/UserInputcontrols and a message-box replacement.MPF.Frontendview-models (MainViewModel,OptionsViewModel,MediaInformationViewModel,CheckDumpViewModel,CreateIRDViewModel).Build & CI
publish-nix.shbuilds and zipsMPF.UI.Avaloniafor every desktop runtime using the same naming scheme as the existing projects (MPF.UI.Avalonia_<framework>_<runtime>_<config>.zip), bundling the dumping tools underPrograms/.workflow_dispatchtrigger and a separate macOS-runner job that publishes the signed universal.appand attaches it to the rolling release. (That job uses macOS CI minutes — happy to gate it behindworkflow_dispatch/tags if you prefer.)macOS-specific fixes to shared code
Each also benefits the CLI on macOS:
DefaultDiscImageCreatorPathnow detects macOS viaRuntimeInformation(OSVersion.PlatformreturnsUnixon macOS, so it previously picked the Linux.outbinary name)._Volumes_DVD_ROM).config.jsonis stored under~/.config/mpfrather than inside the app bundle.WPF-only constructs were replaced inside the new project only (
CustomMessageBox→ a small Avalonia dialog, WinForms file dialogs →StorageProvider,System.Windows.Media→Avalonia.Media,FindResource→ Avalonia resource lookups, etc.).Notes for review
docs/screenshots/purely for this description and can be moved to attachments before merge.rpathauto-fix usesinstall_name_tool(Xcode Command Line Tools); if those are absent, signing still works and only the rpath fix is skipped.macOS app contributed by Knutwurst — https://github.com/knutwurst/MPF