You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Offer IT-published .pkg, .dmg and .zip apps in the macOS Software Catalog - #5603
A macOS "Installer packages" manager (pkg), fed by the catalog itself. A catalog in the machine-wide SoftwareCatalog.json declares a pkgFeed: a folder or mounted share read directly, or an https folder read through the index.json that unigetui pkg-index <folder> writes. Every package in the feed joins that catalog.
Formats:.pkg, .dmg holding an app or a .pkg, and .zip holding an app. Identity, version, architectures and icon are read from the file (xar table of contents and payload, a read-only mounted image, or the zip entries).
Detection: by the package receipt, or by the app in place under its bundle id, so apps already deployed by Jamf or by hand are recognized and updates are offered.
Install and update: a private staged copy is checked against the index hash, its identity and Gatekeeper (Developer ID signed and notarized, unless the catalog sets allowUnsignedPackages), then installed as root behind the macOS administrator prompt. .dmg/.zip apps are swapped into /Applications so that a failure keeps the previous version. Uninstall is refused (new CanUninstall capability).
Without a password:sudo unigetui pkg-helper install-daemon sets up a root LaunchDaemon so standard users can install too. A client sends only catalog id, package id and version over a Unix socket. The helper resolves and checks the package itself, and refuses anything the catalog does not publish or that a standard user could change (catalog, feed, package file, UniGetUI itself). Without it, installs fall back to the prompt.
Catalog Editor: a Package feed dialog sets a catalog's feed and previews what it publishes and which files it skips, with the reason.
Shared changes: InstalledPackagesKeepTheirSource and Sources.CanBeEdited capabilities (replacing Chocolatey and manager-id special cases), and an unpinned catalog entry of a NuGet-based manager now asks the feed for its latest version instead of Version='', so its icon is found.
Docs: docs/PKG_FEEDS.md for administrators, docs/CLI.md, and a section in AGENTS.md.
Testing
UniGetUI.PackageEngine.Tests on macOS: all 99 pkg-feed tests and the NuGet tests pass, including real pkgbuild, productbuild, hdiutil and ditto fixtures, an install over the helper socket, and the refusal cases. The rest of the suite fails the same 107 tests as main on macOS (Windows-only areas).
Tried in the app on macOS with real vendor files: Zoom (.pkg), GitKraken and ChatGPT (.dmg, ChatGPT installed), and the Devolutions Mac downloads (.dmg and .pkg, detected from the existing installs with updates offered), plus the Package feed dialog.
dotnet format whitespace and dotnet format style (--verify-no-changes) pass, and the osx-arm64 NativeAOT publish adds no trim or AOT warnings.
Not run locally: the Windows-only UniGetUI.Tests (catalog model, Catalog Editor and identity tests) and the Chocolatey change, which this PR's CI covers. The package helper has not yet run as a real LaunchDaemon on a Mac.
[P2] Without a receipt, p.Installed is the app's bundle version, but p.Item.Version is the installer component's version. A component at 1.0 containing app 10.2 therefore produces a 10.1 → 1.0 update for installed app 10.1, which UpgradablePackagesLoader.IsPackageValid rejects as a downgrade. Keep update comparisons in the bundle-version domain while retaining the separate installer version for feed selection.
[P2] Changing Location leaves the previous feed's preview visible, and an in-flight inspection can also publish results after the address changes. This shows feed A's packages and trust warning under feed B's address even though Apply saves B. Clear the preview when Location changes and discard results and errors from inspections of an older location.
A failed Check feed operation only changes Summary, which is rendered as an ordinary TextBlock without a live-region announcement. Screen-reader users therefore receive no failure message when the asynchronous check finishes. Announce this error through AccessibilityAnnouncementService with Assertive priority, as the surrounding Catalog Editor does for its errors.
Skip macOS icon conversion when indexing packages on Linux
[P2] Linux builds expose pkg-index, but indexing a readable .pkg or .zip with an embedded icon launches the macOS-only /usr/bin/sips here. Process.Start throws Win32Exception, which the feed/icon readers do not catch, aborting index generation. Skip icon conversion outside macOS so Linux can still index these packages without generated icons.
Deduplicate ZIP icon entries before building the dictionary
[P2] ZIP archives can contain duplicate entry names, so duplicate Contents/Resources/AppIcon.icns entries make ToDictionary throw ArgumentException. Cold folder scans extract ZIP icons, and PkgFeed.IsUnreadable does not catch this exception, so one archive aborts the whole feed scan. Deduplicate icon entry names before constructing the dictionary.
Keep receipt-free update comparisons in the app-version domain
[P2] When receipt-free detection uses the installed app's bundle version, this constructor pairs it with the feed's package/component version, which can use a different numbering scheme. An app update from 2026.2 to 2026.3 inside package version 1.1 passes HasUpdate but is rejected as a downgrade by UpgradablePackagesLoader.IsPackageValid. Keep update comparisons in the app-version domain while retaining the feed's package version separately for installation target selection.
- Every root install command now takes one machine-wide lock (lockf on
a root-only folder in /var/run), so an install the helper runs and one
behind the prompt never share an app's working copies.
- The helper refuses web feeds and packages not served over https: any
user can listen on a loopback port the real server left free.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Take the install lock with lockf's "file command" form, which every
macOS UniGetUI supports, instead of the newer descriptor form.
- Skip icon conversion with sips outside macOS, so pkg-index on Linux
publishes packages without icons instead of failing.
- Tolerate zip archives that repeat an icon entry's name.
- Read the catalog again when the package lists are refreshed.
- Compare an app found in place with the app's version from the feed,
not the package's, and find the package from either version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[P2] The location remains editable during inspection, so checking feed A and switching to feed B can display A's packages and warnings under B's address. Editing the address after completion also leaves the old preview visible. Clear the preview when Location changes and discard both successful and failed results from obsolete inspections before updating the UI.
Announce feed check completion and failures to screen readers
[P2] Feed-check completion and failures only update an ordinary TextBlock bound to Summary; neither the dialog nor this method provides a live-region notification. A screen-reader user activating Check feed is therefore not told whether the asynchronous check finished, skipped files, or failed. Announce the completion summary through AccessibilityAnnouncementService and announce failures assertively, as the existing Catalog Editor does.
…sion
- TrustedPath no longer caches who is an administrator, so an account
removed from the admin group loses its trust at once in the helper.
- When a vendor republishes one package version with a newer app inside,
the newest app wins, in the listing, across feeds, and when the user's
UniGetUI or the helper picks the release to install.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The location field and Browse command remain enabled during this await. If an administrator starts checking feed A and changes the location to feed B before it completes, A's packages, warnings and summary appear beneath B's address, while Apply saves B. Discard both successful and failed inspection results when the current location no longer matches the captured location.
[P2] A matching receipt hides an update when a vendor republishes package 1.0 with app 12.6.1 while receipt 1.0 and app 12.5.0 are installed: this branch compares only the receipt versions. When receipt versions tie, also check the installed bundle and report an available update in the app-version domain, retaining receipt-based detection otherwise.
- Route the package feeds' JSON through PkgJson, which takes the
source-generated JsonTypeInfo explicitly, so the repository's
trim-safety check (MetaTests.TestJsonSerializationOptions) sees it.
- Pin the elevation path in the package-feed tests: the Windows CI
runner is an administrator, which sent the prompt test down the
root path.
- Reject a package whose folders nest more than 64 deep before reading
it recursively, so a crafted file cannot exhaust the stack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Unbounded payload-resource retention can exhaust memory before signature verification, and functional issues remain in update detection, command generation and feed previews.
[P2] Changing Location during or after a check leaves the previous feed's packages, summary and trust warning visible, so an administrator can apply feed B while viewing a successful preview of feed A. Clear inspection state when Location changes and discard results and errors whose captured location no longer matches.
Reject standalone commands for managed package arguments
[P2] Manual-install actions and bundle script export also consume these arguments through GetStandaloneParameters, producing installer -pkg com.example.app -target /. That command cannot install the feed item: -pkg needs a package file path, and DMG/ZIP apps require the managed flow. Override the standalone overload to reject command generation instead of exposing the display-only arguments as an executable command.
Detect newer apps when receipt versions are unchanged
[P2] When a vendor republishes receipt version 1.0 with a newer embedded app, an existing 1.0 receipt makes this branch suppress the update even though CompareReleases selects the newer release. When receipt versions compare equal, check the installed bundle too and report app-version values if the offered app is newer, while preserving receipt-based detection otherwise.
Icons are kept until the app's Info.plist names one, so a payload full of
icons could fill memory. At most 32 icons and 32 MiB are now kept, plus
the named one, and an identity check (as on the root side) reads only
the plist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Root-side logging can write through user-controlled symlinks, and unresolved cancellation, update-detection, and preview-state issues affect correctness.
[P2] Checking feed A and then editing Location to B can display A's packages, warnings, and success summary under B's address, while Apply saves B. Completed previews also remain visible after subsequent edits. Clear the preview when Location changes and discard both results and errors from an obsolete inspection.
Cancellation skips success handling after root install completes
[P2] Canceling during a root install still marks the enclosing operation canceled: AbstractOperation.cs:547–549 overwrites even a successful result, so the app is installed but HandleSuccess() and its inventory updates are skipped. This affects the helper and elevated-process paths too; passing CancellationToken.None only prevents the installer from observing cancellation. Add an engine/UI-supported non-cancellable phase when the root install is submitted, while retaining cancellation before that point.
Detect updates when receipt and package versions match
[P2] A receipt hides updates when a vendor republishes the same package version with a newer embedded app: receipt 1.0 versus package 1.0 reports no update even when the installed app is 12.5.0 and the offered app is 12.6.1. Release selection already distinguishes these builds, but this branch never checks the installed bundle. When receipt versions compare equal, use the bundle versions when available, keeping Installed and Offered in the same version domain.
As root, the helper and the root install set TMPDIR to /tmp, so the
session log went to /tmp/UniGetUI, a path any user could prepare with a
symlink. pkg-helper now runs first in Main, checks for root with
Environment.IsPrivilegedProcess instead of CoreTools, and points TMPDIR
at a root-only folder under /var/run before anything can log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[P2] This lookup always selects the latest release, even when details.Package names an explicitly pinned version. With versions 1.0 and 2.0 in the feed, opening details for a catalog entry pinned to 1.0 sets InstallerUrl, hash and size to 2.0; Download installer then saves 2.0 although the catalog's install action targets 1.0. Pass the concrete requested version (or the offered version for an update), using the latest-release lookup only for unversioned requests.
Bound feed listing timeouts and preserve cached results
[P2] A stalled web feed uses a 60-second timeout, so the enclosing 60-second RunListingTaskWithTimeout can expire before cached fallback returns. Persistent stalls also affect the retry, discarding the whole listing—including healthy feeds—and can remove feed-generated catalog tiles despite a saved index. Bound the complete listing below the engine deadline and return cached or partial results within that budget; shortening each request alone does not bound serial reads across multiple feeds.
Resolve updates across feeds without breaking installation
[P2] If inventory was loaded from feed A and a later update check selects a newer release from feed B, the catalog shows Update but clicking it does nothing: LaunchUpdate looks up the installed instance using a source-sensitive hash. Selecting the newest feed here changes that identity without reloading inventory. Make update resolution honor InstalledPackagesKeepTheirSource == false while retaining the selected update's feed for installation.
Detect app updates when receipt versions are unchanged
[P2] When a vendor reuses receipt version 1.0 but updates its app from 12.5.0 to 12.6.1, this branch reports no update because the installed receipt is also 1.0. The newer app is ignored even though CompareReleases selects that release. Compare installed and offered app versions when the receipt version cannot distinguish releases, while preserving receipt-only detection for packages without an app.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it adds
A macOS "Installer packages" manager (
pkg), fed by the catalog itself. A catalog in the machine-wideSoftwareCatalog.jsondeclares apkgFeed: a folder or mounted share read directly, or an https folder read through theindex.jsonthatunigetui pkg-index <folder>writes. Every package in the feed joins that catalog..pkg,.dmgholding an app or a.pkg, and.zipholding an app. Identity, version, architectures and icon are read from the file (xar table of contents and payload, a read-only mounted image, or the zip entries).allowUnsignedPackages), then installed as root behind the macOS administrator prompt..dmg/.zipapps are swapped into/Applicationsso that a failure keeps the previous version. Uninstall is refused (newCanUninstallcapability).sudo unigetui pkg-helper install-daemonsets up a root LaunchDaemon so standard users can install too. A client sends only catalog id, package id and version over a Unix socket. The helper resolves and checks the package itself, and refuses anything the catalog does not publish or that a standard user could change (catalog, feed, package file, UniGetUI itself). Without it, installs fall back to the prompt.Shared changes:
InstalledPackagesKeepTheirSourceandSources.CanBeEditedcapabilities (replacing Chocolatey and manager-id special cases), and an unpinned catalog entry of a NuGet-based manager now asks the feed for its latest version instead ofVersion='', so its icon is found.Docs:
docs/PKG_FEEDS.mdfor administrators,docs/CLI.md, and a section inAGENTS.md.Testing
UniGetUI.PackageEngine.Testson macOS: all 99 pkg-feed tests and the NuGet tests pass, including realpkgbuild,productbuild,hdiutilanddittofixtures, an install over the helper socket, and the refusal cases. The rest of the suite fails the same 107 tests asmainon macOS (Windows-only areas)..pkg), GitKraken and ChatGPT (.dmg, ChatGPT installed), and the Devolutions Mac downloads (.dmgand.pkg, detected from the existing installs with updates offered), plus the Package feed dialog.dotnet format whitespaceanddotnet format style(--verify-no-changes) pass, and the osx-arm64 NativeAOT publish adds no trim or AOT warnings.UniGetUI.Tests(catalog model, Catalog Editor and identity tests) and the Chocolatey change, which this PR's CI covers. The package helper has not yet run as a real LaunchDaemon on a Mac.