Skip to content

Offer IT-published .exe and .msi installers in the Windows Software Catalog - #5622

Merged
Gabriel Dufresne (GabrielDuf) merged 4 commits into
mainfrom
feature/windows-installer-feeds
Oct 28, 2026
Merged

Gabriel Dufresne (GabrielDuf) merged 4 commits into
mainfrom
feature/windows-installer-feeds

Conversation

@GabrielDuf

@GabrielDuf Gabriel Dufresne (GabrielDuf) commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What it adds

A Windows "Installer packages" manager (installer), the counterpart of the macOS pkg manager from #5603. A catalog's pkgFeed now fills it on Windows too: every .exe and .msi in the feed (a folder, a share, or an https folder read through the index.json that unigetui pkg-index <folder> writes) joins that catalog, and users install, update and uninstall them from the Software Catalog. One feed folder and one index.json can serve both platforms.

  • Formats: .msi is read through msi.dll (Property table, summary Template, product icon) and identified by its UpgradeCode. .exe goes through a managed, bounded PE reader (version resource, sections, appended data, icon) that recognizes Inno Setup, NSIS and WiX Burn with their silent switches. A <name>.json beside a file overrides anything, and gives the silent switches of an installer UniGetUI does not recognize (Advanced Installer, InstallShield, Squirrel…).
  • Detection: from Apps and Features, so apps deployed by Intune, ConfigMgr or by hand are recognized and updates are offered. An .msi matches through Windows Installer (related products, product code); an .exe by the manifest's uninstall key, a bundle's upgrade code, else its display name with an agreeing publisher. A name match found only by stripping a version must share the package's major version, so a side-by-side product (.NET SDK 8 next to 10) is not taken for it.
  • Install, update, uninstall: a private staged copy is checked against the index hash, its identity and Authenticode (WinVerifyTrust, waived only by allowUnsignedPackages), then run silently. An .msi elevates unless it is per-user; an .exe starts unelevated and lets the installer elevate itself, unless its manifest says "scope": "machine". Uninstall runs the registered command made silent; Windows Installer products take their scope from AssignmentType, since per-user MSIs are registered under HKLM too.
  • Catalog Editor: the Package feed dialog now previews Windows feeds, with platform-specific texts.

Shared feed core. The format-agnostic parts of the macOS manager move to UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds (catalog feed declarations, locations, the cached listing, the index schema, sources, staging, pkg-index); the macOS manager keeps its behaviour. Windows entries are listed under their own windowsPackages key in index.json, so no Mac, whatever its release, ever offers a Windows installer. pkg-index runs on both platforms, writes its own list and keeps the other's while its files are unchanged.

Docs: docs/WINDOWS_INSTALLER_FEEDS.md for administrators, docs/PKG_FEEDS.md, docs/CLI.md, and a section in AGENTS.md.

Testing

  • UniGetUI.PackageEngine.Tests on Windows arm64: full suite passes (2,907, with 9 opt-in end-to-end tests skipped); the feed tests (166) also pass on top of the second commit, and the macOS feed tests pass on net10.0 (120).
  • WinInstallerEndToEndTests (opt-in with UNIGETUI_INSTALLER_E2E=1, or =elevated for UAC) install, update and uninstall real MSI and Inno Setup packages built by the tests: folder and web feeds, a tampered file refused, an unsigned one refused, a manifest-driven installer, the app's operation classes, and per-machine installs behind UAC. All passed locally, and nothing was left installed.
  • In the app on Windows, through the headless CLI and the GUI, against the real Devolutions installers (Agent, Gateway, Launcher, MsRdpEx, Password Manager, Server Console, Remote Desktop Manager, UniGetUI): installed apps recognized at the right versions, the RDM update offered, icons read from the installers.
  • dotnet format whitespace and dotnet format style (--verify-no-changes) pass. The win-x64 NativeAOT compile reported no trim or AOT warnings from the new code; its link step could not run on this arm64 machine, so there is no AOT runtime check.
  • Not run: the macOS tests that need Apple tools (disk images, the root helper, pkgbuild) — they still need a run on a Mac; NSIS and WiX Burn installers were not executed (only detected), as the tools are not installed here.

Known limits

  • Installers whose technology is not recognized need a manifest with their silent switches.
  • There is no Windows equivalent of the macOS root helper yet: an install for every user shows a UAC prompt.
  • Elevated installers run from a staging folder under the user's temp directory.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved defects can trigger unnecessary elevation, skip valid installers, select or install incorrect versions, generate invalid standalone commands, and suppress CLI diagnostics.

6 open issues
What changed in this PR

Adds Windows .exe and .msi support to the Software Catalog, extending the macOS installer-feed model introduced in #5603.

Changes:

  • Adds a Windows installer manager with metadata extraction, installed-app detection, signature checks, and managed install/update/uninstall operations.
  • Extracts shared feed infrastructure and supports mixed-platform indexes while keeping Windows entries invisible to older Macs.
  • Integrates Windows feeds into the Catalog Editor and CLI, with compatibility tests and administrator documentation.
File Description
src/​UniGetUI.Windows.slnx Includes the Windows installer and shared feed projects.
src/​UniGetUI.Tests/​SoftwareCatalogTests.cs Recognizes feed-only catalogs on Windows.
src/​UniGetUI.PackageEngine.Tests/​UniGetUI.PackageEngine.Tests.csproj References the new projects for testing.
src/​UniGetUI.PackageEngine.Tests/​MacPkgTests.cs Adapts shared APIs and tests mixed-platform indexes.
src/​UniGetUI.PackageEngine.Tests/​InstallerFeedsTestCollection.cs Defines a nonparallel installer-feed test collection.
src/​UniGetUI.PackageEngine.PackageEngine/​UniGetUI.PackageEngine.PEInterface.csproj Adds shared and Windows-specific manager references.
src/​UniGetUI.PackageEngine.PackageEngine/​PEInterface.cs Registers the Windows installer manager.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​WinInstallerFeedInspector.cs Adds Windows feed previews and indexing.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​WinInstaller.cs Implements package discovery, installed state, and updates.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​UniGetUI.PackageEngine.Managers.WinInstaller.csproj Defines the Windows installer project.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​InternalsVisibleTo.cs Exposes internals to package-engine tests.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​WinInstallerRunner.cs Stages, verifies, and executes installer operations.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​WinFeed.cs Reads Windows folder feeds and web indexes.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​PeReader.cs Extracts bounded executable metadata and icons.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​MsiDatabase.cs Reads MSI metadata and installed-product information.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​InstallReceipts.cs Records differences between feed and registered versions.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​InstallerMetadata.cs Resolves installer metadata and manifest overrides.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​InstallerKind.cs Detects installer technologies and silent switches.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​InstalledMatcher.cs Matches feed packages to installed applications.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​Authenticode.cs Verifies Authenticode trust and identifies signers.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​ArpEntries.cs Reads Apps and Features registrations and scope.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Helpers/​WinInstallerOperationHelper.cs Connects package operations to managed execution.
src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Helpers/​WinInstallerDetailsHelper.cs Supplies package details, icons, and available versions.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​UniGetUI.PackageEngine.Managers.MacPkg.csproj References shared feed infrastructure.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​PkgIndexCommand.cs Adapts macOS indexing to the shared command.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​PkgHelperCommand.cs Uses shared catalog and JSON APIs.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​PkgFeedInspector.cs Uses shared preview models and location validation.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​MacPkg.cs Uses shared feed sources.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​Internal/​PkgInstaller.cs Delegates staging and hash checks to shared code.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​Internal/​PkgIndexApp.cs Converts shared index app metadata to macOS models.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​Internal/​PkgHelperClient.cs Uses shared JSON serialization.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​Internal/​PkgFeedIndex.cs Removes the superseded macOS-only index schema.
src/​UniGetUI.PackageEngine.Managers.MacPkg/​Internal/​PkgFeed.cs Adopts shared listings and filters macOS entries.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds.csproj Defines the shared feed project.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​InternalsVisibleTo.cs Exposes shared internals to tests.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​InstallerFeedSource.cs Shares catalog-owned feed sources across managers.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​FeedStaging.cs Shares copying, downloading, and hash verification.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​FeedLocation.cs Centralizes feed paths, URL checks, and enumeration.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​FeedListing.cs Shares listing caches and last-known fallback behavior.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​FeedJson.cs Shares source-generated JSON helpers.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​FeedInspection.cs Defines shared feed-preview results.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​FeedIndexCommand.cs Writes indexes while preserving unchanged foreign-platform entries.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​FeedIndex.cs Defines separate macOS and Windows index entries.
src/​UniGetUI.PackageEngine.Managers.Generic.InstallerFeeds/​CatalogFeeds.cs Shares catalog feed declaration parsing.
src/​UniGetUI.Avalonia/​Views/​DialogPages/​PkgFeedEditor.axaml Displays platform-specific feed guidance.
src/​UniGetUI.Avalonia/​ViewModels/​SoftwarePages/​SoftwareCatalogViewModel.cs Expands Windows feeds into catalog tiles.
src/​UniGetUI.Avalonia/​ViewModels/​DialogPages/​PkgFeedEditorViewModel.cs Enables Windows previews and platform-specific text.
src/​UniGetUI.Avalonia/​Program.cs Routes Windows pkg-index commands.
src/​UniGetUI.Avalonia/​Models/​SoftwareCatalog.cs Accepts feed-only Windows catalogs.
src/​UniGetUI.Avalonia.slnx Includes the shared feed project.
src/​Languages/​lang_en.json Adds Windows installer and feed-editor strings.
docs/​WINDOWS_INSTALLER_FEEDS.md Documents Windows feed publishing and operation behavior.
docs/​PKG_FEEDS.md Links to Windows feed documentation.
docs/​CLI.md Documents cross-platform indexing behavior.
AGENTS.md Describes shared feeds and Windows installer architecture.

Review effort: Balanced

Comment thread src/UniGetUI.Avalonia/Program.cs Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

ID-normalization collisions silently hide non-Latin applications from the catalog, while scope-insensitive receipts can suppress real updates and incomplete MSI staging can cause installation failures.

1 open issue
2 previously missed issues

In code that hasn't changed since last review

Medium severity Scope receipts by installation type to prevent incorrect update detection

src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​InstallReceipts.cs:25

Per-user and per-machine installations can share an uninstall key and DisplayVersion, so a receipt for one can make the other appear updated and hide a real update. Persist and match entry.PerMachine, and have Record replace receipts by package ID, uninstall key, and scope rather than deleting every receipt for the package. Treat existing receipts without scope as unverified rather than applying them to either installation.

Medium severity Stage or reject MSI packages with external cabinet dependencies

src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​WinInstallerRunner.cs:37

A valid MSI can reference an external cabinet, such as data1.cab beside Product.msi. The metadata reader accepts these packages, but staging copies only the MSI, so msiexec runs from a new directory without its required media and installation fails even when the cabinet is present in the feed. Either stage and verify the referenced payloads too, or detect non-self-contained MSIs during indexing/inspection and report them as unsupported instead of offering an install that cannot succeed.

6 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved issues can suppress updates through stale version receipts, offer MSIs whose required files are not staged, and cause quadratic receipt processing during package inventory.

No open issues 🎉

3 previously missed issues

In code that hasn't changed since last review

Medium severity Remove stale version receipt after successful uninstall

src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Helpers/​WinInstallerOperationHelper.cs:77

Successful uninstall leaves the package's version receipt behind. For example, after installing build 4.2.0.1234 registered as 4.2, uninstalling it here and manually installing an older build also registered as 4.2 makes InstalledVersion report the removed newer build, suppressing its update. Remove the matching receipt after a successful uninstall so later installations cannot inherit that stale version mapping.

Medium severity Reuse indexed receipt snapshot to avoid quadratic inventory scans

src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​InstallReceipts.cs:25

WinInstaller.ListInstalled() calls this once per matched package, and Load() reads and deserializes the entire receipts file each time. With N installed packages and N receipts, each inventory performs N full-file reads/parses plus N linear searches, making receipt resolution quadratic; both installed-package and update listings repeat this work. Load an indexed receipt snapshot once per inventory and reuse it, refreshing it after Record() writes.

Medium severity Inspect all MSI media rows before marking packages self-contained

src/​UniGetUI.PackageEngine.Managers.WinInstaller/​Internal/​MsiDatabase.cs:70

FirstColumn drops empty Cabinet values and stops after 16 nonempty values, so mixed embedded/loose-file MSIs and external cabinets beyond that limit can pass this check. Staging copies only the MSI, so these packages are offered but installation lacks their required files. Inspect all relevant Media/File rows, including file compression attributes, before declaring an MSI self-contained.

1 resolved since last review

Review effort: Balanced

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ This pull request will not be approved automatically: the automated security check could not complete, so a human review is required.

Integration Details
{
	"deliveryId": "e8184d40-d305-11f1-8bb9-0b459941684e",
	"headSha": "38964577870a4e9ea7cf93d81eefc1bf24176872"
}

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉

Integration Details
{
	"deliveryId": "ebca96be-d305-11f1-9902-7147c0f85bde",
	"headSha": "38964577870a4e9ea7cf93d81eefc1bf24176872",
	"reviewer": "copilot-pull-request-reviewer[bot]"
}

@GabrielDuf
Gabriel Dufresne (GabrielDuf) merged commit 5419675 into main Oct 28, 2026
7 checks passed
@GabrielDuf
Gabriel Dufresne (GabrielDuf) deleted the feature/windows-installer-feeds branch October 28, 2026 20:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants