Visitar URL original
Installing Python install manager MSIX for all users · Issue #119 · python/pymanager · GitHub
Skip to content

Installing Python install manager MSIX for all users #119

Description

@zooba

There's a lack of clear information online about how best to install an MSIX for all users of a machine, and a number of approaches that seem to half work.

This issue is to attract discussion until we can figure out the best ways and document them.

Activity

  1. pinned this issue on May 21, 2025
  2. zooba commented on May 21, 2025

    @zooba
    MemberAuthor

    So far I believe (but haven't tested at scale) that the best way to do this is to create a Scheduled Task that runs for all users at login that launches:

    C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command Add-AppxPackage <path to MSIX>
    

    Or alternatively (without needing a pre-downloaded MSIX):

    C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command Add-AppxPackage -AppInstallerFile https://www.python.org/ftp/python/pymanager/pymanager.appinstaller
    

    It unfortunately pops up a command window briefly, but otherwise it works best for ensuring all the aliases are set correctly and it's ready for the user to use. It doesn't seem to have any problems running again if it's already installed, and won't downgrade (though it will fail in this case).


    A close second approach so far is the Add-AppxProvisionedPackage PowerShell command, which when run as admin with an MSIX file, will provision the package and enable auto-install for all users next time they log in:

    Add-AppxProvisionedPackage -Online -PackagePath python-manager-25.0.msix
    

    Unfortunately it seems to have a bug right now where it doesn't correctly replace existing python.exe and python3.exe aliases, even though it reports that they've been updated. Users don't get their first-run experience in this model (unless they do py install --configure) and so won't find out. The other aliases seem to be okay.


    Any other approaches? Has anyone tried using any of the management/deployment tools?

  3. davidldennison commented on Jul 19, 2025

    @davidldennison

    Why would they do this to us 😂 😂 😂

  4. DrusTheAxe commented on Jul 21, 2025

    @DrusTheAxe

    MSIX supports installing a package for all users; the formal term is you 'provision' the package. This makes the package available to all users, compared to registering a package for a user. Here's the definitive way to install + uninstall a package 'per user' and 'for all users'. The simple explanation is...

    • PerUser: Install=Add, Uninstall=Remove
    • AllUsers: Install=Stage+Provision, Uninstall=Deprovision+Remove

    For a more detailed explanation keep reading, but remember, you asked :P If your coffee's low I suggest a refill before proceeding...

    Here's some code snippets with the key APIs. I'l use C# syntax as I'm not as familiar with pywinrt's projections but that's just syntax. It's the APIs that matter:

    Per-User

    Install

    var options = new Windows.Management.Deployment.AddPackageOptions();
    var packageManager = new Windows.Management.Deployment.PackageManager();
    var packageUri = new Uri("file://c:/blah/foo.msix");
    var result = packageManager.AddPackageByUriAsync(packageUri, options).GetAwaiter();

    Uninstall

    string packageFullName = "foo_1.2.3.4_neutral__1234567890abc";
    Windows.Management.Deployment.PackageManager packageManager;
    var result = packageManager.RemovePackageAsync(packageFullName).GetAwaiter();

    All Users

    Install

    var options = new Windows.Management.Deployment.StagePackageOptions();
    var packageManager = new Windows.Management.Deployment.PackageManager();
    var result = packageManager.StagePackageByUriAsync(packageUri, options).GetAwaiter();
    
    string packageFamilyName = "foo_1234567890abc";
    result = packageManager.ProvisionPackageForAllUsersAsync(packageFamilyName);

    Uninstall

    string packageFamilyName = "foo_1234567890abc";
    result = packageManager.DeprovisionPackageForAllUsersAsync(packageFamilyName);
    
    string packageFullName = "foo_1.2.3.4_neutral__1234567890abc";
    Windows.Management.Deployment.PackageManager packageManager;
    var options = Windows.Management.Deployment.RemovalOptions.RemoveForAllUsers;
    var result = packageManager.RemovePackageAsync(packageFullName, options).GetAwaiter();

    The APIs have multiple overloads for behavioral variations but this is representative enough.

    Explanation

    It helps to understand 'install' isn't actually a proper noun for MSIX. To oversimplify...

    • Stage = Lay down the package content on the disk appropriately. Suuuuper oversimplistically: MKDIR pkgdir & UNZIP package.msix to pkgdir & ACL pkgdir
    • Register = Wire up the user profile for the package (create tiles on StartMenu, define file type associations, yadda yadda)

    Stage is done once per machine, and when complete the files are readable but only the Deployment pipeline has write access. This lets us share the files across all users (no need for unique copies of files).

    OTOH Register is a per-user action. To "install" a package for 5 users, ultimately, the package is staged once and registered 5x.

    "Add" is a common operation that does both "Stage this package if necessary + Register it for the user".

    This requires no special permissions, other than the API is called by a Medium IL process (or an AppContainer with the packageManagement capability).

    To 'uninstall' you Remove the package which will Deregister it for the current user and, if there's no other references on the package, it's Destaged (i.e. removed from the system).

    Package Lifetime and References

    MSIX uses a GC style model for package lifetime. A package can be removed only if there are no 'references' to the package. There's several ways to define a reference e.g. Register'ing a package for a user. If a package is registered for 2 users and one Removes the package it's no longer Registered for that user but can't be removed from disk without breaking the other Registered user. When the 2nd user Removes the package its Deregistered and if that's the last reference on the package it's also automagically removed from disk.

    There's several ways a 'reference' can be defined on a package. Register is one. Being listed in the Provisioned list of packages is another. There's a few other ways but this is beyond the scope of this discussion. Just know MSIX uses GC style semantics so packages will be removed from disk when the last reference is removed.

    AllUser Explanation

    Windows has a list of provision packages, alterable with De/Provision APIs. This list affects more than just the current user so altering it requires admin privilege (aka elevation).

    Notice the order of operations. To install we must Stage then Provision because Provision takes a package family name for a package known to Windows at this point. Windows knows about packages once Staged so you can't (successfully) Provision a package before it's Staged (or Added).

    Provisioned packages are registered for users at the next opportunity, usually their next login. At login Windows compares what a user has registered vs what the should have e.g. the Provisioned package list has the Foo package but when Bob logs in he doesn't have Foo registered, so Windows registers it for him during his login. This is why Calculator and other provisioned packages show up on everyone's StartMenu despite never having done anything to specifically request installation.

    On the flipside, during uninstall we Deprovision the package first, to make sure no new user gets it Registered (i.e. avoid timing/race conditions). Deprovisioning the package removes it from the Provisioned list. That's all. There's no change to user Registration, it's simply no longer in the list so new users don't get it offered to them.

    Then once Deprovisioned we call Remove with the RemoveForAllUsers option. This deregisters the package for the current user, and other users logged in. IF a package is registered for a user and they're not logged in, the remove request is tracked for that user and processed when they next login.*

    * Registering a package requires a user be logged in because various resources and APIs used during registration are only available and functional while the user's logged in. And for those thinking of HKCU yes that's one reason, but the simplest one. There's other more complex interactions which can't be done without the user being logged in.

    Powershell Cmdlets

    You can do this via the command line using Powershell cmdlets

    Per-User

    Install

    Add-AppxPackage c:\blah\foo.msix

    Uninstall

    Get-AppxPackage foo | Remove-AppxPackage

    All Users

    Install

    Add-AppxProvisionedPackage -Online -PackagePath c:\blah\foo.msix

    Uninstall

    Remove-AppxProvisionedPackage -Online -PackageName Foo -AllUsers

    The *-AppxProvisionedPackage cmdlets require admin privilege e.g. run from a command run you launched as an admin prompt.

    P.S. The cmdlets also support offline operation against a Windows image. You can also un/install packages via DISM.EXE, various other tools or (for the adventurous) use the WinRT API (above) from Powershell v5 via WinRT invocation. In the end it largely boils down to the same thing as these are functional equivalents to the PackageManager API (and in the online cases, are often literally thin wrappers over the API).

  5. DrusTheAxe commented on Jul 21, 2025

    @DrusTheAxe

    the best way to do this is to create a Scheduled Task that runs for all users at login that launches:

    Ick! No, you don't need to do that.

    A close second approach so far is the Add-AppxProvisionedPackage PowerShell command

    This is the canonical and recommended solution to make a package available for all users.

    Unfortunately it seems to have a bug right now where it doesn't correctly replace existing python.exe and python3.exe aliases, even though it reports that they've been updated.

    You misunderstand - that's a feature, not a bug.

    AppExecutionAliases follow FirstWriterWins semantics. When you install e.g. Python 3.12 as the first Python on the machine the AppExecutionAlias python.exe is created for the 3.12 package and, since it's not already defined for the user, it's enabled. Now install Python 3.13 and a python.exe alias is created for the 3.13 package but, since there's already a 'python.exe' alias the new one is NOT enabled.

    Same is true of python3.exe - first installed for a user is enabled, 2nd+ are defined but user must explicitly enable.

    You can go into Settings > Apps > Advanced app settings > App execution aliases to see which aliases are defined, which are active, disable one if you like, and if there's multiple identical aliases you can choose which one is enabled. That's why you can have 7 versions of Python install with 7 instances of python.exe but of course python.exe will only launch one of them (or non, if they're all disabled).

    If you desire unique aliases for each version of Python you'll need unique aliases e.g. Python 3.12 can define python312.exe, Python 3.13 python313.exe etc (in addition to the shared python.exe and python3.exe)

  6. zooba commented on Jul 28, 2025

    @zooba
    MemberAuthor

    You misunderstand - that's a feature, not a bug.

    AppExecutionAliases follow FirstWriterWins semantics. When you install e.g. Python 3.12 as the first Python on the machine the AppExecutionAlias python.exe is created for the 3.12 package and, since it's not already defined for the user, it's enabled. Now install Python 3.13 and a python.exe alias is created for the 3.13 package but, since there's already a 'python.exe' alias the new one is NOT enabled.

    There's an additional feature known as uap8:AllowOverride that is set on the redirector's aliases (we added it to the OS specifically for this scenario), and it disables the semantics you describe. That flag is being ignored for a system-wide provision, but it works great for a per-user install. The inconsistency there is the bug.

    the best way to do this is to create a Scheduled Task that runs for all users at login that launches:

    Ick! No, you don't need to do that.

    If your only hammer is Group Policy, what's the better option than this? (I know it's not the only hammer, but I keep encountering "IT" "Professionals" who believe it is, and there's only so much arguing I can do with each of them...)

  7. DrusTheAxe commented on Jul 28, 2025

    @DrusTheAxe

    There's an additional feature known as uap8:AllowOverride that is set on the redirector's aliases (we added it to the OS specifically for this scenario), and it disables the semantics you describe.

    Ahhh. You learn something new every day :-)

    That flag is being ignored for a system-wide provision, but it works great for a per-user install. The inconsistency there is the bug.

    :-(

    Known issue
    https://task.ms/57641304

  8. monster739 commented on Jun 6, 2026

    @monster739

    There's a lack of clear information online about how best to install an MSIX for all users of a machine, and a number of approaches that seem to half work.

    This issue is to attract discussion until we can figure out the best ways and document them.

  9. Bill-Stewart commented on Jun 16, 2026

    @Bill-Stewart

    Known issue
    https://task.ms/57641304

    It seems that this link is not available to users that don't have an account (or aren't an external user) in Microsoft's Azure tenant.

    Is there some way us non-Microsoft folks can see the status of this issue?

  10. Bill-Stewart commented on Jun 16, 2026

    @Bill-Stewart

    So far I believe (but haven't tested at scale) that the best way to do this is to create a Scheduled Task that runs for all users at login that launches:

    C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command Add-AppxPackage <path to MSIX>
    ...
    It unfortunately pops up a command window briefly...

    Here's a way around this. AddAppXPackage.js:

    var Args = WScript.Arguments;
    
    if ( Args.Unnamed.Count == 0 ) {
      WScript.Echo("Usage: AddAppXPackage.js <MsixFileName>");
      WScript.Quit();
    }
    
    function scriptPath() {
      return WScript.ScriptFullName.substring(0, WScript.ScriptFullName.length -
        WScript.ScriptName.length);
    }
    
    var FSO = new ActiveXObject("Scripting.FileSystemObject");
    var WshShell = new ActiveXObject("WScript.Shell");
    
    var MsixFileName = Args.Unnamed.Item(0);
    
    // Assume script's directory if not specified
    if ( MsixFileName.indexOf("\\") == -1 ) {
      MsixFileName = FSO.BuildPath(scriptPath(), MsixFileName);
    }
    
    if ( ! FSO.FileExists(MsixFileName) ) {
      WScript.Echo("File not found: " + MsixFileName);
      WScript.Quit(2);  // ERROR_FILE_NOT_FOUND
    }
    
    // FSO.GetSpecialFolder(1) is System32 directory
    var PowerShell = FSO.BuildPath(FSO.GetSpecialFolder(1),
      "WindowsPowerShell\\v1.0\\powershell.exe");
    var CommandLine = PowerShell + " " +
      "-NoProfile -NonInteractive -Command Add-AppxPackage " +
      "'" + MsixFileName + "'";
    
    // 0 = run hidden; false = run asynchronously
    WshShell.Run(CommandLine, 0, false);
    

    Run this using wscript.exe and the MSIX filename as a parameter. You can omit the path of the MSIX file (specify just its name) if the MSIX file sits in the same directory as AddAppxPackage.js.

    Running this script with wscript.exe means there's no initial console/terminal window, and the script itself executes powershell.exe in a hidden window.

  11. zooba commented on Jun 18, 2026

    @zooba
    MemberAuthor

    Is there some way us non-Microsoft folks can see the status of this issue?

    Afraid not, you just need to poke someone with access and ask them to check. But it makes it easier to connect public issues with the internal work so they don't get lost so easily.

  12. deleted a comment from bb7014650-cpu on Sep 18, 2026
  13. deleted a comment from bb7014650-cpu on Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions