Support renamed MakePkg.exe for MSIXVC2 capability detection - #134
Open
Jason Williams (WilliamsJason) wants to merge 6 commits into
Open
Support renamed MakePkg.exe for MSIXVC2 capability detection#134Jason Williams (WilliamsJason) wants to merge 6 commits into
Jason Williams (WilliamsJason) wants to merge 6 commits into
Conversation
The GDK renames Makepkg2.exe to MakePkg.exe, with the new MakePkg.exe absorbing the makepkg2 capabilities. The April 2026 GDK still ships makepkg2.exe as a separate binary, so it remains supported as a fallback. Adds PackageUploader.ClientApi.Tools.IMsixvc2ToolResolver, a reusable, non-UI component that probes 'MakePkg.exe supports uploadsource' first and falls back to the existing makepkg2.exe discovery + probe. It lives in ClientApi so PackageUploader.exe can consume it in a follow-up change. All MSIXVC2 availability/capability decisions in the UI now route through the resolver instead of a bare File.Exists on MakePkg2Path. MakePkg.exe *version* checks (SubVal auto-update / custom SubVal path) are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The probe launches a child process and can block for up to the 5s timeout, twice if MakePkg.exe fails and we fall back to makepkg2.exe. Running that inline in MainPageViewModel and PackageCreationViewModel constructors could freeze the UI for ~10s at startup and again when the packaging page is built - exactly the case a legacy MakePkg.exe that stalls on the unknown 'supports' verb would hit. Both view models now kick the probe off via Task.Run and marshal the result back through the new BaseViewModel.RunOnUiThread helper, which no-ops to an inline call when there is no WPF dispatcher (unit tests). IsMsixvc2Enabled now defaults to false so the MSIXVC2 flow can't be entered before the probe reports back. The on-demand re-resolves on the packaging/upload paths are unchanged, preserving the no-caching semantic. Adds Msixvc2ProbeTask on both view models so tests can await the probe deterministically, plus coverage for the DI extension resolving from a bare ServiceCollection with no logging or UI services registered. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ProcessSelectedPackage() probed for the MSIXVC2 tool synchronously, so selecting an .msixvc package could freeze the UI for up to ~10s (two sequential 5s probes) immediately after the user picked a file. The probe now runs on a background thread and marshals its result back via RunOnUiThread, mirroring the pattern already used in MainPageViewModel and PackageCreationViewModel. The probe result is not display-only: IsUploadReady() gates the Upload command on Msixvc2UnavailableMessage being empty. The message setter now calls CheckCanExecuteUploadCommand() when it changes, so the button's enabled state stays in sync with the async probe. That also fixes a latent ordering bug - IsMsixvc2Package was set before the message, so the single existing CanExecute re-raise fired too early to observe the message. The callback early-returns if IsMsixvc2Package is no longer true, so a stale probe result can't be published after a different package is picked. Clicking Upload while the probe is still in flight remains safe: StartMsixvc2Upload() re-resolves the tool synchronously and, when it comes back missing, raises the error page and returns before setting Package.Msixvc2ToolPath or navigating. Added a test asserting exactly that, plus a non-blocking regression guard on the selection path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Microsoft.Xbox.Packaging.Tools.makepkg2 ships on an internal-only feed, so naming it in user-facing text pointed external customers at something they cannot install. Removed it from the MakePkg2NotFoundErrorMsg resource string (English-only; the ja-JP/ko-KR/zh-CN MainPage resx files do not carry this key) and from the XML docs on Msixvc2Tool and IMsixvc2ToolResolver. Removing the NuGet cache scan would have left PackageUploader.exe with no way to find makepkg2, because the console app calls Resolve() with no hints and self-discovery only looked at the app directory, the current directory, and PATH. The GDK ships MakePkg.exe and makepkg2.exe side by side in its bin directory, so discovery now consults the GDK and the NuGet cache becomes redundant for both hosts. Msixvc2ToolResolver searches app directory, current directory, GDK, then PATH - matching the order MainPageViewModel.ResolveFilePath already uses, so the UI and the command line agree on which binary they will run. GDK roots come from the GameDK environment variable first, then the two registry keys the WPF host already reads. Registry access is guarded by OperatingSystem.IsWindows() rather than #if, keeping the single cross-platform net10.0 assembly and the AOT/CA1416 clean build; Microsoft.Win32.Registry is already in the net10.0 shared framework, so no PackageReference was added. Failures from any source are swallowed as before. The GDK lookup is seamed behind an internal IGdkRootLocator injected via a new internal constructor, so the tests never require an installed GDK and the public IMsixvc2ToolResolver surface is unchanged. MainPageViewModel.ResolveMakePkg2Path collapsed to a bare ResolveFilePath call once its duplicate NuGet scan was removed, so it was inlined. Verified against the real GDK on a dev box from a plain HostApplicationBuilder with no UI services: MakePkg.exe 10.0.26100.7851 exits 2 and makepkg2.exe 2604.405.14000.0 exits 0, resolving to the GDK copy with no NuGet package present. Worth noting the 64-bit HKLM key was empty on that box and only the WOW6432Node mirror was populated, so both keys are load-bearing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The six new files under ClientApi/Tools were the only `#nullable enable` directives in the repository. Nullable reference types are configured project-wide in the .csproj where they are used, and PackageUploader.ClientApi leaves them off, so the directives were a novel pattern here. Removes the directives and strips the reference-type annotations that become invalid without them. Value-type nullables such as `TimeSpan?` are `Nullable<T>` rather than NRT annotations and are unchanged, as are the null-forgiving operator and `is null` / `is not null` patterns. This is annotation-only: no guard, branch, or return value changes. Because the compiler no longer expresses that `null` is the `no capable tool` result, the XML docs are now the sole home of that contract, so every member that can return null states it explicitly in a <returns> tag along with the guarantee that it never throws. Callers such as the command line adapter map a null Resolve() to unavailable and have no handler for an escaping exception. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…d line MainPageViewModel.ResolveFilePath and Msixvc2ToolResolver.Discover were two copies of the same search. Keeping them in sync was a standing hazard, and they had already drifted: the desktop app consulted only the registry for a GDK install, while the command line also honoured the GameDK environment variable. The two hosts could therefore run different binaries on the same machine, which is the class of bug this work item exists to remove. Extracts the discovery layer as a public IToolPathResolver / ToolPathResolver in PackageUploader.ClientApi.Tools and routes both hosts through it. The order is application directory, current directory, the bin directory of any installed GDK, then PATH. Discovery rather than Msixvc2ToolResolver is the shared component because the desktop app also resolves SubmissionValidator.dll, which is not an MSIXVC2 tool. Consolidating onto an MSIXVC2-named type would have made that a permanent special case and misnamed the type on the day it shipped. IGdkRootLocator stays internal. Only the path-resolution contract is public; the environment and registry sources behind it remain an implementation detail, and ToolPathResolver seams them through an internal constructor the same way Msixvc2ToolResolver already seams its probe runner. Msixvc2ToolResolver keeps its probe and fallback logic and delegates discovery. Its internal fourth constructor now takes IToolPathResolver in place of IGdkRootLocator. That constructor is internal, so the public surface of both IMsixvc2ToolResolver and Msixvc2ToolResolver is unchanged and the pending command line adapter needs no edit. Two deliberate behaviour decisions: The desktop app now honours the GameDK environment variable, which it previously ignored. This is a user-visible change. A developer who has pointed GameDK at a GDK should get that GDK from both hosts, and a single implementation cannot preserve both orders. Find reports a miss as null, matching Resolve and the rest of the namespace rather than the empty string ResolveFilePath used to return. Because a non-null argument to Resolve is authoritative and suppresses discovery, the view model coalesces to string.Empty when forwarding a path, which keeps the previous behaviour of not searching twice for a tool it has already located. Discovery tests move down to target ToolPathResolver directly, keeping every case they covered before. The tests that remain against Msixvc2ToolResolver are the ones that genuinely exercise the probe and fallback chain, plus new coverage that discovery is delegated and that an explicit path suppresses it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| { | ||
| return gdkCandidate; | ||
| } | ||
| } |
Collaborator
There was a problem hiding this comment.
Maybe it should search the GDK location first before checking the current working directory. Don't know how possible it is, but what if the current working directory has a matching executable name (MakePkg.exe) and that gets found and executed instead of the GDK
| return null; | ||
| } | ||
|
|
||
| try |
Collaborator
There was a problem hiding this comment.
For this try-catch block, if one of them throw an exception it looks like it'll exit altogether and go into the catch statement. However, what I think this is intending to do is fall back to the next option. If true, might be worth seeing if theres a way to scope each exception to allow it to search the next best candidate, and if all fail throw a catch-all error
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Why
The GDK is renaming
Makepkg2.exetoMakePkg.exe— the newMakePkg.exereplaces the older tool and absorbs its MSIXVC2 capabilities. The GDK still shipsmakepkg2.exealongside it, so we must keep supporting it as a fallback.Everywhere the code decided "is makepkg2.exe available and does it support MSIXVC2?", the resolution order is now:
MakePkg.exe supports uploadsource. Exit code0== supported. A legacyMakePkg.exefails this (non-zero exit / error) — that's the discriminator.makepkg2.exe, discovered the same way and probed with the same verb.Both binaries are discovered in this order: application directory → current directory → installed GDK →
PATH. The GDK step looks in<GDKInstallPath>\bin, with the install root taken from theGameDKenvironment variable first, thenHKLM\SOFTWARE\Microsoft\GDK\Installed Rootsand itsHKLM\SOFTWARE\WOW6432Node\...mirror (valueGDKInstallPath). This is a single shared implementation (IToolPathResolver) that both the desktop app and the command line call, so the two hosts cannot disagree about which binary they will run.Why one order serves both tools: the GDK ships
MakePkg.exeandmakepkg2.exeside by side in<GDKInstallPath>\bin. Measured on a dev box with the current GDK installed:supports uploadsource...\Microsoft GDK\bin\makepkg.exe10.0.26100.7851...\Microsoft GDK\bin\makepkg2.exe2604.405.14000.0That is exactly the case the fallback exists for, and it means a GDK user needs no separately installed tool at all.
MakePkg.exeversion checks (PackageCreationViewModel's_supportsSubValAutoUpdate/_supportsCustomSubValPathviaFileVersionInfo) are deliberately unchanged — those are version gates, not capability probes.New shared component
The resolution + probe logic lives in
PackageUploader.ClientApi(the only project referenced by bothPackageUploader.UIandPackageUploader.Application) so that a follow-up PR adding MSIXVC2 upload support toPackageUploader.execan consume it directly. All new files are confined to a newsrc/PackageUploader.ClientApi/Tools/folder; no existing ClientApi file was renamed or restructured, and no.csprojchange was required.Public surface
Internal surface (not public API)
The GDK lookup is seamed so the tests never require an installed GDK. It stays
internal(visible toPackageUploader.ClientApi.Testvia the existingInternalsVisibleTo) and is not part of the consumable API — only the path-resolution contract is public, not the environment and registry sources behind it:IMsixvc2ToolResolverandMsixvc2ToolResolver's public surface is unchanged — same four interface members, same three public constructors.IMsixvc2ToolResolver.csis byte-identical to its previous revision, and the diff onMsixvc2ToolResolver.cstouches no line containingpublic. Only the internal fourth constructor changed, swappingIGdkRootLocatorforIToolPathResolver. Anything already binding to this interface needs no adjustment.Notes for consumers:
nullis the "no capable tool" result, and it is a documented contract rather than a compiler-enforced one.PackageUploader.ClientApidoes not enable nullable reference types (no project in the repo uses file-level#nullabledirectives), so these signatures carry no?annotations. Every member that can returnnullsays so in its XML docs, together with the guarantee that it never throws — callers branch on the return value and do not need a try/catch around resolution.CreateNoWindow = true,UseShellExecute = false. Exceptions are logged and swallowed, never thrown.PackageUploader.execallsResolve()with no hints and would otherwise have found nothing unless the tool happened to be onPATH.Microsoft.Win32.Registryis in the plainnet10.0shared framework, so this needs noPackageReferenceand no-windowstarget.OperatingSystem.IsWindows()check rather than#if, which keeps CA1416 satisfied in the single cross-platformIsAotCompatibleassembly. On non-Windows only the environment variable is consulted.new Msixvc2ToolResolver()works with zero DI, so a console app can construct it directly, or callservices.AddMsixvc2ToolResolver().UI changes
All MSIXVC2 availability/capability decisions now route through the resolver instead of a bare
File.Exists(MakePkg2Path):MainPageViewModel— takesIMsixvc2ToolResolverandIToolPathResolver;IsMakePkg2Enabled→IsMsixvc2Enabled,MakePkg2UnavailableErrorMessage→Msixvc2UnavailableErrorMessage; logs which tool provided support.ResolveFilePath,FindFileInPath, andResolveMakePkg2Pathare all removed — the first two are replaced by the sharedIToolPathResolver, and the third collapsed to a bare lookup once its duplicate cache scan was gone. The now-deadusing Microsoft.Win32goes with them.Msixvc2UploadViewModel— the ~40-line inlineSupportsUploadSourceFlag()process probe is replaced byResolveMsixvc2Tool()delegating to the resolver.PackageUploadViewModel—MakePkg2UnavailableMessage→Msixvc2UnavailableMessage; MSIXVC2 detection andStartMsixvc2Upload()use the resolver.PackageCreationViewModel—IsMakePkg2Available→IsMsixvc2Available; theUseMsixvc2pack branch resolves the tool rather than assumingMakePkg2Path.FileVersionInfoversion checks untouched.Msixvc2UploadingViewModel,PackageModel—MakePkg2Path→Msixvc2ToolPath.App.xaml.cs—services.AddMsixvc2ToolResolver().New files:
src/PackageUploader.ClientApi/Tools/—IMsixvc2ToolResolver.cs,Msixvc2Tool.cs,Msixvc2ToolResolver.cs,IToolProbeRunner.cs,Msixvc2ToolResolverExtensions.cs,GdkRootLocator.cs,IToolPathResolver.cs,ToolPathResolver.cs; and testssrc/PackageUploader.ClientApi.Test/Msixvc2ToolResolverGdkDiscoveryTest.cs,Msixvc2ToolResolverExtensionsTest.cs,ToolPathResolverTest.cs.One discovery implementation, shared by both hosts
MainPageViewModel.ResolveFilePathandMsixvc2ToolResolver.Discoverwere two copies of the same search, and keeping them in sync was a standing hazard. They had already drifted: the UI consulted only the registry for a GDK install, while ClientApi also honoured theGameDKenvironment variable. Two hosts could therefore run different binaries on the same machine — the exact class of bug this work item exists to remove.The discovery layer is now a single public
IToolPathResolverthat both hosts call.Msixvc2ToolResolver. The UI also resolvesSubmissionValidator.dll, which is not an MSIXVC2 tool and never will be. Consolidating onto anMsixvc2*-named type would have made that a permanent special case and misnamed the type on day one.InternalsVisibleTo. ClientApi is not shipped as a NuGet package — CI onlydotnet publishesPackageUploader.Application.csproj— so there are no external consumers and no semver obligation. The repo's three existingInternalsVisibleTogrants are all production→its own test project; this would have been the first production→production grant.GameDKenvironment variable, which it previously ignored. A developer who has explicitly pointedGameDKat a GDK should get that GDK from both hosts, and one implementation cannot preserve both orders. On a stock install both routes resolve to the same place, so this is only observable where the env var and the registry disagree.Findreports "not found" asnull, matchingResolveand the rest of the namespace, rather than the empty stringResolveFilePathreturned. Because a non-null argument toResolveis authoritative and suppresses discovery, the view model coalesces tostring.Emptywhen forwarding a path — preserving the existing behaviour of not searching twice for a tool it has already located.Probes do not block the UI thread
The capability check launches a child process, so unlike the
File.Existstest it replaced it can take seconds. Every probe that runs before the user has asked for anything now runs off the UI thread via a newBaseViewModel.RunOnUiThreadhelper, with the observable property initialised to a safe default and updated when the probe completes:MainPageViewModelandPackageCreationViewModelconstructors — otherwise app start and packaging-page construction could each freeze for up to the full two-probe timeout.PackageUploadViewModel.ProcessSelectedPackage()— same hazard immediately after the user picks a file.Because
IsUploadReady()gates the Upload command onMsixvc2UnavailableMessage, that property's setter re-raisesCanExecuteChanged, so the button's enabled state cannot go stale relative to an async probe. A stale-result guard prevents a slow probe from publishing against a package the user has since changed.Probes that run at the moment of acting (
StartMakePackageProcess,StartMsixvc2Upload,Msixvc2UploadViewModel.StartPackAndUploadAsync) remain synchronous — they must have a definitive answer before launching the tool. Clicking Upload while a background probe is still in flight is safe:StartMsixvc2Upload()re-resolves synchronously and, when the tool is missing, raises the error page and returns before setting any tool path or navigating.Resource strings
MainPage.resx/.Designer.cs— the keyMakePkg2NotFoundErrorMsgis kept and only its value reworded:This single key now backs five call sites:
MainPageViewModel,PackageUploadViewModel(×2),Msixvc2UploadViewModel, andPackageCreationViewModelviaLayoutParseError. Two of those were hardcoded literals onmainand are now resx-backed. Three render as an error dialog on the "tool missing" path, which is the most likely place this text is ever read, so the wording is written to stand alone rather than only as inline UI text.The localized
MainPage.ja-JP/.ko-KR/.zh-CNresx files do not carry this key — it is English-only, so there is no localized resource to update.Tests
The existing
.bat-file fake-tool style inMsixvc2UploadViewModelTest.csstill works. New/updated coverage:Resolution order (
Msixvc2UploadViewModelTest.cs): newMakePkg.exesupports the verb → used, no fallback; legacyMakePkg.exefails butmakepkg2.exesucceeds → fallback used; both fail → unavailable; both missing → unavailable;MakePkg.exehangs → timeout, falls back;MakePkg.exepath missing → falls back;MakePkg.exepath is a directory → unavailable; upload arguments includeuploadsource.GDK discovery (
Msixvc2ToolResolverGdkDiscoveryTest.cs, new):MakePkg.exefrom GDKbin;makepkg2.exefallback from GDKbin; GDK preferred overPATHwhen both would satisfy the probe; GDK-binmiss falls through toPATH; no GDK and nothing onPATH→ clean null;GameDKbeats registry; registry fallback;WOW6432Nodefallback; no source → empty; throwing sources → empty, nothing escapes; real (default) readers do not throw, covering the non-Windows registry skip; and an assembly-wide assertion that noToolsconstant names an internal package feed.PATHis replaced with a controlled temp directory per test and restored, and the fixture reports inconclusive rather than passing misleadingly if a real tool sits in the app or current directory and would pre-empt GDK discovery.DI from a non-UI host (
Msixvc2ToolResolverExtensionsTest.cs, new):AddMsixvc2ToolResolver()resolves from a bareServiceCollectionwith no logging and no UI services registered — strictly harsher than theHostApplicationBuilderthe console app uses.Off-thread probes: regression guards asserting the
MainPageViewModel/PackageCreationViewModelconstructors and the package-selection path all return well inside the probe duration, plus a test that clicking Upload while a probe is in flight produces a clean error with no navigation and no tool launch.Existing view-model tests were updated for the new ctor parameter.
Additionally, resolution was verified end-to-end against a real GDK install from a plain
HostApplicationBuilderwith no UI services — the exact shape the follow-up console-app PR uses — producing the exit codes in the table above and resolving to the GDK copy ofmakepkg2.exe.Validation
The test executables are invoked directly because the repo's
global.jsonopts into Microsoft.Testing.Platform. Note that--nologois a VSTest option that Microsoft.Testing.Platform does not accept — passing it todotnet testhere fails the run rather than being ignored.No
global.json,packages.lock.json, or.csprojchanges are included in this PR.Scope
No changes to
src/PackageUploader.Applicationorsrc/PackageUploader.UI/Model/Xvc/XvcFile.cs— those belong to the concurrent MSIXVC2-in-PackageUploader.exePR, which will consumeIMsixvc2ToolResolverfrom this change.