PSResourceGet icon indicating copy to clipboard operation
PSResourceGet copied to clipboard

Allow to specify custom module path in Install-PSResource

Open cmenzi opened this issue 2 years ago • 17 comments

Summary of the new feature / enhancement

Install-Module and now also Install-PSResourceGet do not allow to specify the Path where Modules are installed. It only works via Save-Module or Save-PSResource.

It would be great if there is a environment variable or parameter to control where powershell modules are installed when using Scope CurrentUser

Proposed technical implementation details (optional)

No response

cmenzi avatar Nov 16 '23 19:11 cmenzi

cc @SydneyhSmith since this seems to be a PSResourceGet issue.

StevenBucher98 avatar Dec 18 '23 18:12 StevenBucher98

@SydneyhSmith would it be possible to proritize this? I'm from Azure Data and we have very significant support costs in our org related to modules being in OneDrive. I can also reach out internally if needed.

KirillOsenkov avatar Apr 04 '24 03:04 KirillOsenkov

@KirillOsenkov

There is also the Save-PSResource -IncludeXml -Path 'your\custom\path' cmdlet, it already does this.

As long as the path is in PSModulePath Import-Module will work.

o-l-a-v avatar Apr 04 '24 05:04 o-l-a-v

@o-l-a-v Yes, this is also what I mentioned in the issue. But nobody will remember this all the time.

You usually go to PowerShell Gallery and copy&paste the install-psresource Az and run it. There are also scripts, where somebody ensures that a certain module is installed.

I would really prefer an environment variable or a one-time setting Set-PSDefaultResourceLocation, ..

cmenzi avatar Apr 04 '24 05:04 cmenzi

@cmenzi

Oops, yep, also mentioned in 1st post.

This is how PowerShell have worked since forever. We've requested the ability to choose path for years. I don't see it happening any time soon. Thus Save-PSResource and set PSModulePath is currently the best workaround IMO.

I'd like PSResourcePath to just use the first path in PSModulePath in user context if choosing CurrentUser as scope (and PSModulePath env variable in system context if scope "Machine").

o-l-a-v avatar Apr 04 '24 05:04 o-l-a-v

@o-l-a-v

This is how PowerShell have worked since forever.

This module PSResourceGet would have been the chance to change that, isn't it :-). I mean it's something new that could also behave it bit different. I mean every command name has change from Install-Module to Install-PSResource, ...

Why not changing a bit the behavoir or adding one parameter?

cmenzi avatar Apr 04 '24 06:04 cmenzi

This is blocking PowerShell/PowerShell/issues/15552 from moving forward.

For far longer than PowerShell has been around, the localappdata and programdata folders have been the standard way to store application data for user and computers.

Please, make the change so that this can move on.

benwa avatar Jul 09 '24 14:07 benwa

For far longer than PowerShell has been around, the localappdata and programdata folders have been the standard way to store application data for user and computers.

In windows, there is more than windows to think about with a change like this

BlackV avatar Jul 12 '24 01:07 BlackV

How about a new environment variable for given scope (AllUsers vs. CurrentUser), say PSResourceGetInstallPathOverride, that can be set with a new cmdlet Set-PSResourceInstallPathOverride -Path <path> -Scope <CurrentUser|AllUsers>, which then should be used if present by Install-PSResource and Update-PSResource with fallback to current default behavior?

Easy to implement. Should not introduce breaking changes. Don't have to wait for PowerShell to change anything. More reasoning:

Click to view

Considerations

Changing default behavior is breaking

  • After almost two decades (PowerShell first appeared in November 2006) with current default behavior, changing the default behavior of how and where PowerShell modules and scripts are installed will be a breaking change with unknown consequences.
  • Years and years of documentation, Reddit (and forum posts) and blogs will suddenly be both deprecated and misleading for new users.
  • How would we test this breaking change? When would we be happy with the test data and make the decision to change default behavior?
  • Even though most agree "MyDocuments" isn't a suitable place to store PowerShell modules, we should try to avoid breaking changes with the consequences already mentioned.

Current default behavior is worse for Windows than Unix

  • Default install path for scripts and modules on Unix is not MyDocuments.
  • But the ability to override default path would be nice for Unix too.

Can't reliably use PSModulePath env variable for override

  • Can contain multiple paths. How to choose what path to use? First index (see next bullet point)? Alphabetical?
  • Other 3rd parties like Scoop adds their path to the front of PSModulePath, so can't blindly choose the first path.
    • [System.Environment]::GetEnvironmentVariable('PSModulePath','User').Split([System.IO.Path]::PathSeparator)[0].

Can't use PSModulePath in powershell.config.json with Windows PowerShell 5.1

It'd probably be the best option if it also worked for Windows PowerShell 5.1.

About this option here:

Suggested here:

Confirmed it does not work for Windows PowerShell 5.1 here:

No need for Install-PSResource -Path

  • It already exist in Save-PSResource -IncludeXml -Path.
  • Specifying path every time you install and update a module is not a good user experience.
    • Prone to errors.
    • What to do if a not yet used path is specified?
      • Warn that this directory is empty, require confirmation to proceed? Override with -Force?
      • If directory does not exist, should it be created?
  • It won't make PowerShell able to import module or use script as they will not be added to PSModulePath and PATH.
    • Or should Install-PSResource -Path just fix that too automagically?

Proposal

New environment variable PSResourceGetInstallPathOverride

  • Can only contain one path.
  • Automatic subdirectories \Modules and \Scripts.
  • If PSResourceGetInstallPathOverride exist in the scope that Install-PSResource or Update-PSResource is run: Use it. Else use default behavior.

New cmdlet Set-PSResourceInstallPathOverride

  • Syntax: Set-PSResourceInstallPathOverride -Path '<valid_path>' -Scope 'CurrentUser/AllUsers'
  • Logic:
    1. If running as administrator and -Scope CurrentUser: Throw, else next step does not make sense.
    2. Try to create directory if it does not already exist. Don't continue if it fails.
      • Also create subdirectories \Modules and \Scripts
      • If all directories already exist, try to create and delete a dummy directory to make sure we have sufficient permissions.
    3. Set PSResourceGetInstallPathOverride environment variable in given scope.
    4. Add PSResourceGetInstallPathOverride\Modules to PSModulePath and PSResourceGetInstallPathOverride\Scripts to PATH in given scope to ensure PowerShell will be able to find the resources.
  • Rerun with same parameters: Repair the override by checking all changes are still present.

Change to Utils.cs -> GetPathsFromEnvVarAndScope

  • Return value of environment variable PSResourceGetInstallPathOverride (+ \Modules and \Scripts) if present and directory exists.

o-l-a-v avatar Jul 12 '24 09:07 o-l-a-v

I've started a draft PR ^ where the basics already work:

image

It can be tested by cloning the branch and build the module with for instance:

& .\build.ps1 -Clean -Build -BuildFramework net472

Then import the built DLL with for instance:

Import-Module -Name 'C:\Users\olavb\Git\Others\PowerShell--PSResourceGet\out\Microsoft.PowerShell.PSResourceGet\Microsoft.PowerShell.PSResourceGet.dll'

Waiting for some response from PSResourceGet maintainers before spending more time on it. I might try to add some more functionality while I wait.

o-l-a-v avatar Jul 13 '24 12:07 o-l-a-v

I think it should just look at powershell.config.json (if it exists) and use the PSModulePath there (if it exists), and otherwise fall back to what it is now.

If there is a powershell.config.json in Split-Path $Profile with a "PSModulePath" use that for "CurrentUser" ...

If there is a powershell.config.json in in $PSHome with a "PSModulePath" use that for "AllUsers" ...

Those values will be in the PSModulePath, unless the user removes them, after startup.

Jaykul avatar Aug 20 '24 03:08 Jaykul

@Jaykul

https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_config?view=powershell-7.4#psmodulepath

Setting PSModulePath in powershell.config.json says it "Overrides the PSModulePath settings for this PowerShell session.".

Sounds to me you either set PSModulePath env variable, or as a setting in this JSON?

That's not a great solution either. If it overrides PSModulePath the environment variable, then you'd potentially want multiple paths here too. Which path should PSResourceGet default to, the first one?

o-l-a-v avatar Aug 21 '24 05:08 o-l-a-v

You're misunderstanding it, @o-l-a-v -- I mean, I'm not saying the docs are great, but just try it.

Each config file (one in $PSHome and one in Split-Path $Profile) overrides one of the paths that PowerShell ADDS to the PSModulePath environment variable (for all future sessions). It's only used at the start of each session -- so if you change your PSModulePath in your profile (as I do), you might not even notice, but here's how it works if you don't have a profile:

Set your PSModulePath environment variables to short strings we can identify:

[System.Environment]::SetEnvironmentVariable("PSModulePath", "PATH3", "User")
[System.Environment]::SetEnvironmentVariable("PSModulePath", "PATH4", "Machine")

Start a new PowerShell instance (e.g. a new tab in Windows Terminal), the PSModulePath will be something like this:

C:\Users\Jaykul\Documents\PowerShell\Modules;C:\Program Files\PowerShell\Modules;c:\program files\powershell\7\Modules;PATH3;PATH4

Now set the user config file. It's important that you understand you can only put a single path to a folder in this string, to replace the native path (which would be a "Modules" folder adjacent to the config file). You can use other environment variables with %ComSpec% syntax, but you cannot put multiple folders with a path separator. For demonstration purposes, we'll again use a short string we can identify:

$path = Join-Path (Split-Path $Profile) powershell.config.json
$config = @{}
if (Test-Path $path) {
   $config = Get-Content $path | ConvertFrom-Json -AsHashtable
}
$config.PSModulePath = "PATH1"
$config | ConvertTo-Json | set-content $path

And start a new PowerShell instance (e.g. a new tab in Windows Terminal), the PSModulePath will be something like this:

PATH1;C:\Program Files\PowerShell\Modules;c:\program files\powershell\7\Modules;PATH3;PATH4

Finally, just to finish the demo, set the PSHome config:

$path = Join-Path $PSHOME powershell.config.json
$config = @{}
if (Test-Path $path) {
   $config = Get-Content $path | ConvertFrom-Json -AsHashtable
}
$config.PSModulePath = "PATH2"
$config | ConvertTo-Json | set-content $path

And start a final PowerShell instance (you may want to run it -noprofile because it's going to be super broken, with no modules available, including PSReadLine). The PSModulePath will be:

PATH1;C:\Program Files\PowerShell\Modules;PATH2;PATH3;PATH4

Hopefully it's clear how those configs interact with the environment variables, and why reading them makes sense (with a fallback to the default of a "Modules" folder next to the config file path, if they're not set).

Incidentally, I find it really weird that the only path that cannot be overridden points at a folder that doesn't even exist on my systems.

Jaykul avatar Aug 22 '24 04:08 Jaykul

Oh, okay. Thats nice. And should work the same on all platforms. I should've tried rather than trusting the docs. Thanks for the very detailed explaination @Jaykul. 😊

Edit: But it can't be used with Windows PowerShell 5.1, which PSResourceGet also supports.

o-l-a-v avatar Aug 22 '24 06:08 o-l-a-v

@Jaykul - Yes thank you, I've banged my head against that before: it looks like it works at first, namely that the first entry in $PSModulePath is changed, but installing a module still installs to the old, default path, but now import-module can't find it. (The docs even warn that the powershellget commandlets don't pay attention to this setting)

IMO this makes it worse-than-useless (because it breaks existing functionality for no trade-off). If you can get the rest of the ecosystem to honor powershell.config.json (and get the docs updated!), please do!

OranguTech avatar Aug 29 '24 15:08 OranguTech

@OranguTech wrote:

IMO this makes it worse-than-useless (because it breaks existing functionality for no trade-off). If you can get the rest of the ecosystem to honor powershell.config.json (and get the docs updated!), please do!

That's definitely my goal 😉

There's an issue in the ModuleFast repo too. They are not following this setting yet, but they do (by default) install the modules in %LOCALAPPDATA%\powershell\Modules so that's what I have mine set to...

Jaykul avatar Aug 30 '24 23:08 Jaykul

For reference I've made a function that will be incorporated into moduleFast that provides how I envision PowerShell should be providing the info, hopefully a similar PR will reveal a public API for this. https://github.com/PowerShell/PowerShell/issues/15552#issuecomment-2327851719

In summary and testing:

  1. Changing both system and user powershell.config.json results in both paths being modified to PSModulePath, however system always still keeps the original AllUsers ModulePath, while the CurrentUser module path gets replaced (this is desirable behavior e.g. to get away from OneDrive)
  2. Defaults to CurrentUser unless explicitly meant for AllUsers (since AllUsers almost certainly requires admin rights so it shouldn't be the default target)

JustinGrote avatar Sep 04 '24 03:09 JustinGrote

Obviously not a solution to the core issue, but tired with this issue and many others, I've implemented a standalone solution to this:

https://github.com/PowershellFrameworkCollective/PSFramework.NuGet

Allows defining your own scopes or overriding the default ones. Scopes can be static paths or dynamically calculated paths. And you can bootstrap it without needing PowerShellGet:

iwr https://raw.githubusercontent.com/PowershellFrameworkCollective/PSFramework.NuGet/refs/heads/master/bootstrap.ps1 | iex

And then:

Register-PSFModuleScope -Name Personal -Path C:\code\Modules -Description 'Personal local modules, not redirected to OneDrive documents or to some network share'
Install-PSFModule -Name EntraAuth -Scope Personal

Or:

# Once only
Register-PSFModuleScope -Name CurrentUser -Path C:\code\Modules -Description 'Personal local modules, not redirected to OneDrive documents or to some network share' -Persist

# From then on (including in all future consoles)
Install-PSFModule -Name EntraAuth

FriedrichWeinmann avatar May 07 '25 09:05 FriedrichWeinmann