Skip to content

✨ Use PowerShell internals for default installation location determination #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 46 additions & 28 deletions ModuleFast.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ $SCRIPT:DefaultSource = 'https://pwsh.gallery/index.json'

enum InstallScope {
CurrentUser
AllUsers
}


Expand Down Expand Up @@ -254,54 +255,50 @@ function Install-ModuleFast {
begin {
trap {$PSCmdlet.ThrowTerminatingError($PSItem)}

# Setup the Destination repository
$defaultRepoPath = $(Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'powershell/Modules')

# Get the current PSModulePath
$PSModulePaths = $env:PSModulePath.Split([Path]::PathSeparator, [StringSplitOptions]::RemoveEmptyEntries)

#Clear the ModuleFastCache if -Update is specified to ensure fresh lookups of remote module availability
if ($Update) {
Clear-ModuleFastCache
}

if ($Scope -eq [InstallScope]::CurrentUser) {
$Destination = 'CurrentUser'
$defaultRepoPath = $(Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'powershell/Modules')
if (-not $Destination) {
#Special function that will retrieve the default module path for the current user
$Destination = Get-PSDefaultModulePath -AllUsers:($Scope -eq 'AllUsers')

#Special case for Windows to avoid the default installation path because it has issues with OneDrive
$defaultWindowsModulePath = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'PowerShell/Modules'
if ($IsWindows -and $Destination -eq $defaultWindowsModulePath -and $Scope -ne 'CurrentUser') {
Write-Debug "Windows Documents module folder detected. Changing to $defaultRepoPath"
$Destination = $defaultRepoPath
}
}

if (-not $Destination) {
$Destination = $defaultRepoPath
} elseif ($IsWindows -and $Destination -eq 'CurrentUser') {
$windowsDefaultDocumentsPath = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'PowerShell/Modules'
$Destination = $windowsDefaultDocumentsPath
# if CurrentUser and is on Windows, we do not need to update the PSModulePath or the user profile.
# this allows for a similar experience to Install-Module and Install-PSResource
$NoPSModulePathUpdate = $true
$NoProfileUpdate = $true
throw 'Failed to determine destination path. This is a bug, please report it, it should always have something by this point.'
}

# Autocreate the default as a convenience, otherwise require the path to be present to avoid mistakes
if ($Destination -eq $defaultRepoPath -and -not (Test-Path $Destination)) {
if (Approve-Action 'Create Destination Folder' $Destination) {
# Require approval to create the destination folder if it is not our default path, otherwise this is automatic
if (-not (Test-Path $Destination)) {
if ($configRepoPath -or
$Destination -eq $defaultRepoPath -or
(Approve-Action 'Create Destination Folder' $Destination)
) {
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
}
}

$Destination = Resolve-Path $Destination

if (-not $NoPSModulePathUpdate) {
if ($defaultRepoPath -ne $Destination -and $Destination -notin $PSModulePaths) {
Write-Warning 'Parameter -Destination is set to a custom path not in your current PSModulePath. We will add it to your PSModulePath for this session. You can suppress this behavior with the -NoPSModulePathUpdate switch.'
$NoProfileUpdate = $true
}
# Get the current PSModulePath
$PSModulePaths = $env:PSModulePath.Split([Path]::PathSeparator, [StringSplitOptions]::RemoveEmptyEntries)

$addToPathParams = @{
Destination = $Destination
NoProfileUpdate = $NoProfileUpdate
}
if ($PSBoundParameters.ContainsKey('Confirm')) {
$addToPathParams.Confirm = $PSBoundParameters.Confirm
#Only update if the module path is not already in the PSModulePath
if ($Destination -notin $PSModulePaths) {
Add-DestinationToPSModulePath -Destination $Destination -NoProfileUpdate:$NoProfileUpdate -Confirm:$Confirm
}
Add-DestinationToPSModulePath @addtoPathParams
}

#We want to maintain a single HttpClient for the life of the module. This isn't as big of a deal as it used to be but
Expand Down Expand Up @@ -2160,6 +2157,27 @@ function Approve-Action {
return $ThisCmdlet.ShouldProcess($Target, $Action)
}

#Fetches the module path for the current user or all users.
#HACK: Uses a private API until https://github.com/PowerShell/PowerShell/issues/15552 is resolved
function Get-PSDefaultModulePath ([Switch]$AllUsers) {
$scopeType = [Management.Automation.Configuration.ConfigScope]
$pscType = $scopeType.
Assembly.
GetType('System.Management.Automation.Configuration.PowerShellConfig')

$pscInstance = $pscType.
GetField('Instance', [Reflection.BindingFlags]'Static,NonPublic').
GetValue($null)

$getModulePathMethod = $pscType.GetMethod('GetModulePath', [Reflection.BindingFlags]'Instance,NonPublic')

if ($AllUsers) {
$getModulePathMethod.Invoke($pscInstance, $scopeType::AllUsers) ?? [Management.Automation.ModuleIntrinsics]::GetPSModulePath('BuiltIn')
} else {
$getModulePathMethod.Invoke($pscInstance, $scopeType::CurrentUser) ?? [Management.Automation.ModuleIntrinsics]::GetPSModulePath('User')
}
}

#endregion Helpers

### ISSUES
Expand Down
25 changes: 15 additions & 10 deletions ModuleFast.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -554,19 +554,17 @@ Describe 'Install-ModuleFast' -Tag 'E2E' {

#TODO: Possibly mock this so we don't touch the testing system documents directory
It 'Destination CurrentUser installs to $HOME\Documents\PowerShell\Modules' {
try {
Remove-Item $HOME\Documents\PowerShell\Modules\PrereleaseTest -Recurse -Force -ErrorAction SilentlyContinue
Install-ModuleFast @imfParams 'PrereleaseTest' -Destination CurrentUser
Resolve-Path $HOME\Documents\PowerShell\Modules\PrereleaseTest -EA Stop
} finally {
Remove-Item $HOME\Documents\PowerShell\Modules\PrereleaseTest -Recurse -Force -ErrorAction SilentlyContinue
if (-not $profile) {
Set-ItResult -Skipped -Because 'This test is not supported when $profile is not set'
}
$winConfigPath = Join-Path (Split-Path ($profile.CurrentUserAllHosts)) 'powershell.config.json'
if (-not $IsWindows -or (Test-Path $winConfigPath)) {
Set-ItResult -Skipped -Because 'This test is not supported on non-Windows or when powershell.config.json is present'
}
}

It 'Scope CurrentUser installs to $HOME\Documents\PowerShell\Modules' {
try {
Remove-Item $HOME\Documents\PowerShell\Modules\PrereleaseTest -Recurse -Force -ErrorAction SilentlyContinue
Install-ModuleFast @imfParams 'PrereleaseTest' -Scope CurrentUser
Install-ModuleFast @imfParams 'PrereleaseTest' -Destination CurrentUser
Resolve-Path $HOME\Documents\PowerShell\Modules\PrereleaseTest -EA Stop
} finally {
Remove-Item $HOME\Documents\PowerShell\Modules\PrereleaseTest -Recurse -Force -ErrorAction SilentlyContinue
Expand Down Expand Up @@ -756,7 +754,15 @@ Describe 'Install-ModuleFast' -Tag 'E2E' {

Describe 'GitHub Packages' {
It 'Gets Specific Module' {
if (-not (Get-Command Get-Secret -ea 0)) {
Set-ItResult -Skipped -Because 'SecretManagement Not Present'
return
}
$credential = [PSCredential]::new('Pester', (Get-Secret -Name 'ReadOnlyPackagesGithubPAT'))
if (-not $credential) {
Set-ItResult -Skipped -Because 'No ReadOnlyPackagesGithubPAT Credential'
return
}
$actual = Install-ModuleFast @imfParams -Specification 'PrereleaseTest=0.0.1' -Source 'https://nuget.pkg.github.com/justingrote/index.json' -Credential $credential -Plan
$actual.Name | Should -Be 'PrereleaseTest'
$actual.ModuleVersion | Should -Be '0.0.1'
Expand All @@ -770,4 +776,3 @@ Describe 'Install-ModuleFast' -Tag 'E2E' {
}
}
}