PowerShell / PSResourceGet

PSResourceGet is the package manager for PowerShell
https://www.powershellgallery.com/packages/Microsoft.PowerShell.PSResourceGet
MIT License
483 stars 92 forks source link

Allow to specify custom module path in Install-PSResource #1494

Open cmenzi opened 10 months ago

cmenzi commented 10 months ago

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

StevenBucher98 commented 8 months ago

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

KirillOsenkov commented 5 months ago

@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.

o-l-a-v commented 5 months ago

@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.

cmenzi commented 5 months ago

@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, ..

o-l-a-v commented 5 months ago

@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").

cmenzi commented 5 months ago

@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?

benwa commented 2 months ago

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.

BlackV commented 2 months ago

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

o-l-a-v commented 2 months ago

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: * 2021-08-12 * 2024-08-20 * 2024-08-20 * 2024-08-22 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 '' -Scope 'CurrentUser/AllUsers'` * Logic: 1. If running as administrator and `-Scope CurrentUser`: Throw, else next step does not make sense. 1. 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. 1. Set `PSResourceGetInstallPathOverride` environment variable in given scope. 1. 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 commented 2 months ago

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.

Jaykul commented 3 weeks ago

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.

o-l-a-v commented 3 weeks ago

@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?

Jaykul commented 3 weeks ago

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.

o-l-a-v commented 3 weeks ago

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.

OranguTech commented 2 weeks ago

@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!

Jaykul commented 1 week ago

@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...

JustinGrote commented 1 week ago

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)