diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml index e9e83d85..baade9bc 100644 --- a/.github/workflows/build-native.yml +++ b/.github/workflows/build-native.yml @@ -36,7 +36,7 @@ concurrency: jobs: native: - uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.12.0 + uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.13.0 permissions: contents: write pull-requests: write diff --git a/claude.md b/claude.md index a54d0d51..3cf0bec2 100644 --- a/claude.md +++ b/claude.md @@ -621,7 +621,7 @@ the comment there about not caching "nothing staged" asks for. `native/CMakeLists.txt`, `PlatformHeads`, `PlatformHeads.Core`, `PlatformHeads.Linux` (the Linux tray's) and `PlatformHeads.WindowsForms` in `Directory.Packages.props`, which move together, and the `PlatformHeads` package - in `native/swift/Package.swift`, all at 0.12.0. So is the native runtime under both heads' drawing: the C++ + in `native/swift/Package.swift`, all at 0.13.0. So is the native runtime under both heads' drawing: the C++ one `deview.cpp` calls as `ph::` (the window, GLFW's input as it happened, which frames are built and drawn, the renderer, the fonts and fontconfig, and the label helpers) and the Swift `PlatformHeadsMac` the AppKit head imports (`HeadWindow`, `EventPump`, `TerminateGuard`, @@ -655,12 +655,16 @@ the comment there about not caching "nothing staged" asks for. minimised on Windows, through `ViewerInput.Unseen`). `ViewerProgram.Loop` keeps its own `hidden` and the head's `unseen` apart and slows `OwnerLink`, `TrackedWatch` and `DocumentWatch` on either. Version 13 is `deview_on_display_lost`, which the Linux shim hands to PlatformHeads' - `ph::OnDisplayLost`, and the Swift one exports and never calls. + `ph::OnDisplayLost`, and the Swift one hands to PlatformHeadsMac's `TerminateGuard` as its + `sessionEnding`, with no bump, since the managed side already called it on every platform. - A logoff stages an owned queue on every platform, as a clean exit does, since none reaches the loop's own shutdown. `ViewerProgram.SessionEnding` does it, once, touching no window, and is - heard three ways: on Windows through `HeadForm.SessionEnding`, which saves the placement after + heard four ways: on Windows through `HeadForm.SessionEnding`, which saves the placement after it; on Linux as the X server goes, through `deview_on_display_lost`, which `NativeViewerWindow` - takes as `ILoopHooks.SessionEnding` and from inside which Xlib then calls `exit(1)`; and through + takes as `ILoopHooks.SessionEnding` and from inside which Xlib then calls `exit(1)`; on macOS + through the same callback, at a logout, restart or shutdown, from inside the present that pumped + AppKit's quit, after which AppKit ends the process: refused, as it was, the quit cancelled the + logout, and a quit from the Dock still is, reported as a close; and through SIGTERM and SIGHUP, which `ViewerProgram` holds PlatformHeads.Core's `SessionEndSignals` for, since .NET ends the process on either with no `finally` run. The signals do not save the placement, because they come on a thread of their own. A display lost inside a mutation cannot @@ -741,7 +745,7 @@ the comment there about not caching "nothing staged" asks for. native library of its own, `diffengine_tray` (decision G), so a tray change never moves the viewer's ABI version. It is a second product of `native/swift`, built by the same `build-native` run, which collects it into the head's `runtimes` through PlatformHeads' `mac-libraries`, and - committed as the viewer's is. The ABI is `native/include/detray.h`, `DETRAY_VERSION` 1: the + committed as the viewer's is. The ABI is `native/include/detray.h`, `DETRAY_VERSION` 2: the menu goes over as one array of `DetrayItem`s, each with its parent by index, since a C struct cannot hold a list of itself, and the Swift side (`Sources/Detray`) makes it a tree again. Its managed half is in the core (`Detray`, `MacTray`), as ProcessEx's native calls are, so the @@ -751,8 +755,11 @@ the comment there about not caching "nothing staged" asks for. AppKit is driven from the main thread only, so the loop is `TrayLoop.RunPumped`, on the thread `Main` starts on, with the library's pump as its wait; nothing in `Program.Inner` awaits before the loop returns. The app is an accessory, - with no Dock icon, App Nap is off (`NapGuard`), and a logout is `TerminateGuard`'s, which - refuses the quit and reports it, for the loop to end as Exit does. "Run at startup" is a + with no Dock icon, App Nap is off (`NapGuard`), and a quit is `TerminateGuard`'s. A logout, + restart or shutdown runs `TraySessionEnd.For` from inside the pump (`detray_on_session_end`, + `MacTray.SessionEnding`, set before `detray_init` makes the guard) and is let through, since + refusing it cancelled the logout; any other quit is refused and reported (`detray_quitting`), + for the loop to end as Exit does. "Run at startup" is a launch agent. Packed as `DiffEngineTray.osx-x64` and `.osx-arm64` - The tray ships as .NET 10 RID specific tool packages (decision F), by `src/DiffEngineTray.Core/TrayHead.targets`, which every head imports after naming its RIDs in diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index 1bb3d06b..3497304f 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -15,7 +15,7 @@ include(FetchContent) FetchContent_Declare( platformheads GIT_REPOSITORY https://github.com/SimonCropp/PlatformHeads.git - GIT_TAG 0.12.0 + GIT_TAG 0.13.0 GIT_SHALLOW ON) FetchContent_MakeAvailable(platformheads) include(${platformheads_SOURCE_DIR}/cmake/PlatformHeads.cmake) diff --git a/native/include/detray.h b/native/include/detray.h index c6f08748..a3ebf1b9 100644 --- a/native/include/detray.h +++ b/native/include/detray.h @@ -3,7 +3,7 @@ * A library of its own beside the viewer's (diffengine_viewer), built from the same Swift package, * so a change to either never moves the other's version. * - * Followed by native/swift/Sources/Detray and src/DiffEngineTray.Mac/Detray.cs. Bump + * Followed by native/swift/Sources/Detray and src/DiffEngineTray.Core/Detray.cs. Bump * DETRAY_VERSION whenever a struct changes or a field changes meaning: the managed side refuses a * library whose version is not the one it was built for. * @@ -20,7 +20,15 @@ extern "C" { #endif -#define DETRAY_VERSION 1 +/* + * 1: the status item and its menu. + * 2: detray_on_session_end, and a logout, restart or shutdown no longer reported by + * detray_quitting. A library from before cancels the logout that asked it to quit. + */ +#define DETRAY_VERSION 2 + +/* What detray_on_session_end calls. */ +typedef void (*DetraySessionEnd)(void); /* PlatformHeads' TrayMenuFlags and PlatformHeadsMac's StatusMenuFlags, which write and read them. */ #define DETRAY_ENABLED 1 @@ -70,9 +78,21 @@ DETRAY_API void detray_pump(void); */ DETRAY_API int32_t detray_poll(char* buffer, int32_t capacity); -/* 1 once AppKit has been asked to quit, which for an app with no Dock icon is a logout. */ +/* + * 1 once AppKit has been asked to quit other than by the session ending: with no Dock icon, by + * something that sent the app a quit event, such as AppleScript's quit. Refused, for the managed + * side to end its loop the way Exit does. + */ DETRAY_API int32_t detray_quitting(void); +/* + * Has ended called at a logout, restart or shutdown, from inside the detray_pump that dispatched + * AppKit's quit, on the main thread. AppKit ends the process as it returns, so it is the one + * chance to stage the queue: refused, the quit would cancel the logout. Read by detray_init, so + * set before it; with none, a session's end is refused and reported by detray_quitting. + */ +DETRAY_API void detray_on_session_end(DetraySessionEnd ended); + /* * The menu as the last apply left it, a line an item, as text, written to buffer with its whole * length returned. For tests, which can make no status item: neither this nor detray_apply needs diff --git a/native/include/deview.h b/native/include/deview.h index 5dd72502..0ce79bfc 100644 --- a/native/include/deview.h +++ b/native/include/deview.h @@ -435,7 +435,10 @@ typedef struct DeviewPlacement { * they shipped together. And panX and panY are kept inside what either pane can show, where * they were kept inside what the dragged one can. * 13: deview_on_display_lost. No struct moved: a new entry point, which a library from before - * does not have, and which the managed side calls on every native window. + * does not have, and which the managed side calls on every native window. The macOS library + * came to call it at a logout, restart or shutdown with no bump: the managed side already + * handed it the same session end on every platform, and either side with the other of a + * different age does what it did before. */ #define DEVIEW_VERSION 13 @@ -520,8 +523,9 @@ DEVIEW_API void deview_shutdown(void); * Has lost called once as the connection to the X server is lost, which is how a logoff ends the * Linux viewer: Xlib ends the process with exit(1) as it returns, so it is the one chance to stage * an owned queue. Called on the thread inside deview_present or deview_poll_input, from inside that - * call. NULL stops it. macOS never calls it: a logout's quit there is the app's own, which reaches - * the clean exit. + * call. NULL stops it. macOS calls it at a logout, restart or shutdown, from inside the + * deview_present that pumped AppKit's quit, and AppKit ends the process as it returns: refused, + * the quit would cancel the logout. A quit from the Dock is still reported as a close. */ DEVIEW_API void deview_on_display_lost(DeviewDisplayLost lost); diff --git a/native/swift/Package.swift b/native/swift/Package.swift index c27aa94b..3a3cc753 100644 --- a/native/swift/Package.swift +++ b/native/swift/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ // What every AppKit head does underneath its drawing, shared with BuildMonitor. The sixth // pin of PlatformHeads, moved with the other five. - .package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.12.0") + .package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.13.0") ], targets: [ // Exists only to import the ABI. Swift does not guarantee struct layout, so the structs diff --git a/native/swift/Sources/Detray/Exports.swift b/native/swift/Sources/Detray/Exports.swift index 270f003a..d1e70c08 100644 --- a/native/swift/Sources/Detray/Exports.swift +++ b/native/swift/Sources/Detray/Exports.swift @@ -75,6 +75,11 @@ public func detrayQuitting() -> Int32 { Tray.shared.quitting ? 1 : 0 } +@_cdecl("detray_on_session_end") +public func detrayOnSessionEnd(_ ended: DetraySessionEnd?) { + Tray.shared.sessionEnded = ended +} + @_cdecl("detray_describe") public func detrayDescribe(_ buffer: UnsafeMutablePointer?, _ capacity: Int32) -> Int32 { Utf8Buffer.write(StatusMenuItem.describe(Tray.shared.item.items), buffer, capacity) diff --git a/native/swift/Sources/Detray/Tray.swift b/native/swift/Sources/Detray/Tray.swift index c9844b5b..9a2511bd 100644 --- a/native/swift/Sources/Detray/Tray.swift +++ b/native/swift/Sources/Detray/Tray.swift @@ -1,4 +1,5 @@ import AppKit +import CDetray import PlatformHeadsMac /// The status item, and what has come of it since the managed side last asked: the items chosen, @@ -11,11 +12,17 @@ final class Tray { private var chosen: [String] = [] private(set) var quitting = false - /// A logout, for an app with no Dock icon to quit from, turned into the managed side's own - /// quit, which stages the queue on its way out. Held here: AppKit keeps its delegate weakly. - private lazy var terminateGuard = TerminateGuard { [unowned self] in - self.quitting = true - } + /// What `detray_on_session_end` was handed: the managed side's session end, which stages the + /// queue it owns and removes the version marker. + var sessionEnded: DetraySessionEnd? + + /// A quit sent to the app, which has no Dock icon to quit from, turned into the managed side's + /// own quit, which stages the queue on its way out. A logout, restart or shutdown runs + /// `sessionEnded` there and then instead, and AppKit ends the process once it returns. Made in + /// `start`, from whatever `sessionEnded` is by then: with none, a session's end is refused as a + /// quit is, which cancels the logout but loses nothing. Held here: AppKit keeps its delegate + /// weakly. + private var terminateGuard: TerminateGuard? private init() { item.chose = { [unowned self] entry in @@ -31,7 +38,13 @@ final class Tray { func start() -> Bool { let application = NSApplication.shared application.setActivationPolicy(.accessory) - application.delegate = terminateGuard + let terminate = TerminateGuard( + { [unowned self] in + self.quitting = true + }, + sessionEnding: sessionEnded.map { ended -> () -> Void in { ended() } }) + terminateGuard = terminate + application.delegate = terminate application.finishLaunching() napGuard.begin(reason: "Tracking what test runs leave pending") return item.start() diff --git a/native/swift/Sources/Deview/Exports.swift b/native/swift/Sources/Deview/Exports.swift index f9672137..16c6811b 100644 --- a/native/swift/Sources/Deview/Exports.swift +++ b/native/swift/Sources/Deview/Exports.swift @@ -148,10 +148,11 @@ public func deviewShutdown() { } } -/// Never called here: a logout's quit is the app's own (TerminateGuard), which reaches the clean -/// exit. Only the Linux shim can lose its display out from under it. +/// Here there is no display to lose: the session's end is a quit from AppKit, at a logout, restart +/// or shutdown, which TerminateGuard hands to `lost` from inside the pump and then lets through. @_cdecl("deview_on_display_lost") public func deviewOnDisplayLost(_ lost: DeviewDisplayLost?) { + Runtime.shared.sessionEnding = lost } /// Renders into a bitmap of this side's own making rather than asking the view for one, with diff --git a/native/swift/Sources/Deview/Runtime.swift b/native/swift/Sources/Deview/Runtime.swift index debe7414..13ad1300 100644 --- a/native/swift/Sources/Deview/Runtime.swift +++ b/native/swift/Sources/Deview/Runtime.swift @@ -13,14 +13,24 @@ final class Runtime { private var delegate: WindowDelegate? - /// A quit from the Dock or a logout, reported as a close, which the managed side answers the - /// way it answers the window's close button: it leaves the loop, persists the queue it owns, - /// and exits. macOS has no tray to hold the queue instead. Held here because + /// A quit from the Dock, reported as a close, which the managed side answers the way it answers + /// the window's close button: it leaves the loop, persists the queue it owns, and exits. macOS + /// has no tray to hold the queue instead. + /// + /// A logout, restart or shutdown runs `sessionEnding` instead, there and then, and AppKit ends + /// the process once it returns: refused, the quit would cancel the logout. Held here because /// `NSApplication.delegate` does not keep it alive. - private lazy var terminateGuard = TerminateGuard { [unowned self] in + private lazy var terminateGuard = TerminateGuard({ [unowned self] in self.input.closeRequested = 1 + }) { [unowned self] in + self.sessionEnding?() } + /// What `deview_on_display_lost` was handed: the managed side's session end, which stages the + /// queue it owns and saves the placement. It hands one over on every window, before the first + /// present, so before any event that could be a logout is dispatched. + var sessionEnding: DeviewDisplayLost? + private let napGuard = NapGuard() private var size = CGSize(width: 1100, height: 700) private var title = "DiffEngineViewer" diff --git a/src/DiffEngineTray.Core/Detray.cs b/src/DiffEngineTray.Core/Detray.cs index 8385b599..1a696b66 100644 --- a/src/DiffEngineTray.Core/Detray.cs +++ b/src/DiffEngineTray.Core/Detray.cs @@ -11,7 +11,7 @@ static class Detray /// Must match DETRAY_VERSION in native/include/detray.h, so a stale library is reported rather /// than read as garbage. /// - public const int ExpectedVersion = 1; + public const int ExpectedVersion = 2; /// /// Has PlatformHeads' resolver find under runtimes/{rid}/native for these @@ -44,6 +44,9 @@ public static void Resolve() => [DllImport(Library, EntryPoint = "detray_quitting")] public static extern int Quitting(); + [DllImport(Library, EntryPoint = "detray_on_session_end")] + public static extern unsafe void OnSessionEnd(delegate* unmanaged ended); + [DllImport(Library, EntryPoint = "detray_describe")] public static extern int Describe(byte[] buffer, int capacity); diff --git a/src/DiffEngineTray.Core/DiffEngineTray.Core.csproj b/src/DiffEngineTray.Core/DiffEngineTray.Core.csproj index 0bc4378a..471b8303 100644 --- a/src/DiffEngineTray.Core/DiffEngineTray.Core.csproj +++ b/src/DiffEngineTray.Core/DiffEngineTray.Core.csproj @@ -9,6 +9,8 @@ --> net10.0 + + true false false diff --git a/src/DiffEngineTray.Core/MacTray.cs b/src/DiffEngineTray.Core/MacTray.cs index 5281e852..b2f1cf0f 100644 --- a/src/DiffEngineTray.Core/MacTray.cs +++ b/src/DiffEngineTray.Core/MacTray.cs @@ -11,16 +11,42 @@ sealed class MacTray : ITray // An id is a path or a snapshot's key with a word either side, which this holds with room over readonly byte[] chosen = new byte[64 * 1024]; + /// + /// What a logout, restart or shutdown runs: , which stages the + /// queue and removes the version marker. Called from inside , on the thread + /// the loop runs on, and AppKit ends the process as it returns, so nothing after the pump runs. + /// Static, since the library calls back through a plain function pointer, which carries no + /// instance to find it on, and settable after , since what it stages is + /// made after the tray. + /// + public static Action? SessionEnding { get; set; } + MacTray() { } + /// + /// Nothing may be thrown back into the library, which is inside AppKit's quit. + /// + [UnmanagedCallersOnly] + static void SessionEnded() + { + try + { + SessionEnding?.Invoke(); + } + catch (Exception exception) + { + Log.Error(exception, "Failed while the session was ending"); + } + } + /// /// The item in the status bar, with every icon and glyph handed over. Null with an /// when there is no library for this machine, one of another version, /// or no status bar to put the item in. /// - public static MacTray? Open(out string? error) + public static unsafe MacTray? Open(out string? error) { try { @@ -32,6 +58,8 @@ sealed class MacTray : ITray return null; } + // Before Init, which reads it: with none, a logout is refused and cancelled + Detray.OnSessionEnd(&SessionEnded); if (Detray.Init() == 0) { error = "AppKit gave the status item no button to draw in."; @@ -127,8 +155,10 @@ public static void Pump() => Detray.Pump(); /// - /// AppKit was asked to quit. With no Dock icon, that is a logout, which the library turned - /// into this, for the loop to end the way Exit does, staging the queue on its way out. + /// AppKit was asked to quit other than by the session ending. With no Dock icon, that is a quit + /// event sent to the app, such as AppleScript's, which the library refused and turned into + /// this, for the loop to end the way Exit does, staging the queue on its way out. A session's + /// end is . /// public static bool Quitting => Detray.Quitting() != 0; diff --git a/src/DiffEngineTray.Mac/Program.cs b/src/DiffEngineTray.Mac/Program.cs index 5e3d1da4..958dd2dc 100644 --- a/src/DiffEngineTray.Mac/Program.cs +++ b/src/DiffEngineTray.Mac/Program.cs @@ -87,11 +87,14 @@ static async Task Inner() owned.Start(); } - // A logout reaches the loop as a quit (MacTray.Quitting), and ends it the way Exit does. - // These are a session's processes being ended otherwise, which .NET answers by ending the - // process with nothing below run + // A logout, restart or shutdown is heard inside the pump (MacTray.SessionEnding), and AppKit + // ends the process as it returns, since refusing the quit would cancel the logout. The + // signals are a session's processes being ended otherwise, which .NET answers by ending + // the process with nothing below run + var sessionEnding = TraySessionEnd.For(owned, TrayVersionFile.Delete); + MacTray.SessionEnding = sessionEnding; using var sessionEnd = new SessionEndSignals( - TraySessionEnd.For(owned, TrayVersionFile.Delete), + sessionEnding, _ => Log.Error(_, "Failed while the session was ending")); var listener = PiperServer.TryBind(out var bindError); diff --git a/src/DiffEngineTray.Mac/runtimes/osx-arm64/native/libdiffengine_tray.dylib b/src/DiffEngineTray.Mac/runtimes/osx-arm64/native/libdiffengine_tray.dylib index c0d6de62..4d77e3cb 100644 Binary files a/src/DiffEngineTray.Mac/runtimes/osx-arm64/native/libdiffengine_tray.dylib and b/src/DiffEngineTray.Mac/runtimes/osx-arm64/native/libdiffengine_tray.dylib differ diff --git a/src/DiffEngineTray.Mac/runtimes/osx-x64/native/libdiffengine_tray.dylib b/src/DiffEngineTray.Mac/runtimes/osx-x64/native/libdiffengine_tray.dylib index c0d6de62..4d77e3cb 100644 Binary files a/src/DiffEngineTray.Mac/runtimes/osx-x64/native/libdiffengine_tray.dylib and b/src/DiffEngineTray.Mac/runtimes/osx-x64/native/libdiffengine_tray.dylib differ diff --git a/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib b/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib index de73d690..9ab3fc12 100644 Binary files a/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib and b/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib differ diff --git a/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib b/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib index de73d690..9ab3fc12 100644 Binary files a/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib and b/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib differ diff --git a/src/DiffEngineViewer/ILoopHooks.cs b/src/DiffEngineViewer/ILoopHooks.cs index 23d89795..542b9702 100644 --- a/src/DiffEngineViewer/ILoopHooks.cs +++ b/src/DiffEngineViewer/ILoopHooks.cs @@ -4,11 +4,11 @@ /// Windows a present pumps messages - which is where user32 runs its own modal loops, for dragging /// a scrollbar thumb and for moving or sizing the window, and where a logoff ends the session. On /// Linux a present reads the X server's events, and a logoff ends the process from inside it as the -/// server goes. The loop's next step never comes in any of these, so the head is handed those steps +/// server goes. On macOS a present pumps AppKit's events, and a logout's quit ends the process from +/// inside it. The loop's next step never comes in any of these, so the head is handed those steps /// to run itself. /// -/// Optional. WinForms takes both; the native heads only , which the -/// macOS one never calls, since a logout's quit there is the app's own. The Linux one has no modal +/// Optional. WinForms takes both; the native heads only . The Linux one has no modal /// loops to be caught in. AppKit has: a menu, which is deliberate, and a window being resized, /// which still draws the rows sliced for its old size until the mouse comes up. Handing that head /// a frame from inside the loop would take a callback in the C ABI. @@ -27,8 +27,8 @@ interface ILoopHooks /// /// The session is ending: stop taking arrivals and stage what the queue holds, before /// returning. Windows may end the process as soon as a window has answered the message that - /// says so, and Xlib ends it as soon as its handler for the lost server returns, both before - /// the loop would get as far as its own shutdown. + /// says so, Xlib ends it as soon as its handler for the lost server returns, and AppKit as soon + /// as the quit's delegate does, all before the loop would get as far as its own shutdown. /// Action? SessionEnding { set; } } diff --git a/src/DiffEngineViewer/Native/NativeViewerWindow.cs b/src/DiffEngineViewer/Native/NativeViewerWindow.cs index cbd28c95..d55f10a7 100644 --- a/src/DiffEngineViewer/Native/NativeViewerWindow.cs +++ b/src/DiffEngineViewer/Native/NativeViewerWindow.cs @@ -181,7 +181,10 @@ public Func? Frame /// /// Heard as the X server goes, which on Linux is how a logoff ends the process, from inside - /// whichever call was reading its events. The macOS shim never calls it. + /// whichever call was reading its events. On macOS it is heard at a logout, restart or + /// shutdown, from inside the present that pumped AppKit's quit, and AppKit ends the process + /// once it returns: PlatformHeadsMac's TerminateGuard, which would otherwise cancel the + /// logout by refusing the quit. /// public unsafe Action? SessionEnding { diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 65b73ddd..919c07e5 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -19,10 +19,10 @@ --> - - - - + + + +