diff --git a/PSFramework.NuGet/PSFramework.NuGet.psd1 b/PSFramework.NuGet/PSFramework.NuGet.psd1 index 1941625..f32d76f 100644 --- a/PSFramework.NuGet/PSFramework.NuGet.psd1 +++ b/PSFramework.NuGet/PSFramework.NuGet.psd1 @@ -3,7 +3,7 @@ RootModule = 'PSFramework.NuGet.psm1' # Version number of this module. - ModuleVersion = '0.9.12' + ModuleVersion = '0.9.16' # ID used to uniquely identify this module GUID = 'ad0f2a25-552f-4dd6-bd8e-5ddced2a5d88' diff --git a/PSFramework.NuGet/changelog.md b/PSFramework.NuGet/changelog.md index a3f22d9..464b8f9 100644 --- a/PSFramework.NuGet/changelog.md +++ b/PSFramework.NuGet/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 0.9.16 (2025-05-17) + ++ Upd: Install-PSFPowerShellGet - now allows bootstrapping localhost without requiring elevation. ++ Fix: Publish-PSFResourceModule - does not include files with brackets (`[]`) in their name. ++ Fix: Save-PSFResourceModule - does not include empty folders or files when using V3 repositories. ++ Fix: Find-PSFModule - fails (with error) when searching for prerelease versions on a default Windows PowerShell console without any modifications. + ## 0.9.12 (2025-05-06) + Fix: Install-PSFModule - fails to install on a default Windows PowerShell console without any modifications. diff --git a/PSFramework.NuGet/en-us/strings.psd1 b/PSFramework.NuGet/en-us/strings.psd1 index 3a53387..0bbe15e 100644 --- a/PSFramework.NuGet/en-us/strings.psd1 +++ b/PSFramework.NuGet/en-us/strings.psd1 @@ -8,6 +8,8 @@ 'Copy-Module.Error.StagingFolderCopy' = 'Failed to deploy module to staging directory when trying to publish {0}' # $Path 'Copy-Module.Error.StagingFolderFailed' = 'Failed to create staging folder when trying to publish {0}' # $Path + 'Find-PSFModule.AllowPrerease.NotSupported' = 'Cannot search for prerelease modules - PowerShellGet is too old! To fix this, use "Install-PSFPowerShellGet -Type V2Latest, V3Latest" and start a new console.' # + 'Install-PSFModule.Error.Installation' = 'Failed to install {0}' # $Name -join ',' 'Install-PSFModule.Error.NoComputerValid' = 'Unable to establish ANY remote connections to {0}' # $ComputerName -join ', 'Install-PSFModule.Error.Setup' = 'Failed to prepare to install {0}' # $Name -join ',' @@ -65,6 +67,7 @@ 'Save-PowerShellGet.Error.UnableToResolve' = 'Unable to resolve aka.ms link: {0}. Make sure internet access is available!' # $link + 'Save-PSFModule.AllowPrerease.NotSupported' = 'Cannot install prerelease modules - PowerShellGet is too old! To fix this, use "Install-PSFPowerShellGet -Type V2Latest, V3Latest" and start a new console.' # 'Save-PSFModule.Error.NoComputerValid' = 'Failed to connect to any of the provided computer targets: {0}' # ($ComputerName -join ', ') 'Save-PSFResourceModule.Deploying' = 'Deploying {2} from resource module {0} ({1}) to {3}' # $module.Name, $versionFolder.Name, $item.Name, $pathEntry diff --git a/PSFramework.NuGet/functions/Get/Find-PSFModule.ps1 b/PSFramework.NuGet/functions/Get/Find-PSFModule.ps1 index 7e870ba..2d84ca9 100644 --- a/PSFramework.NuGet/functions/Get/Find-PSFModule.ps1 +++ b/PSFramework.NuGet/functions/Get/Find-PSFModule.ps1 @@ -131,52 +131,56 @@ $param = $PSBoundParameters | ConvertTo-PSFHashtable -Include Name, Repository, Tag, Credential, IncludeDependencies } process { - #region V2 - if ($script:psget.V2 -and $Type -in 'All', 'V2') { + #region V3 + if ($script:psget.V3 -and $Type -in 'All', 'V3') { $paramClone = $param.Clone() - $paramClone += $PSBoundParameters | ConvertTo-PSFHashtable -Include AllVersions, AllowPrerelease - if ($Version) { - if ($convertedVersion.Required) { $paramClone.RequiredVersion = $convertedVersion.Required } - if ($convertedVersion.Minimum) { $paramClone.MinimumVersion = $convertedVersion.Minimum } - if ($convertedVersion.Maximum) { $paramClone.MaximumVersion = $convertedVersion.Maximum } + $paramClone += $PSBoundParameters | ConvertTo-PSFHashtable -Include AllowPrerelease -Remap @{ + AllowPrerelease = 'Prerelease' } + if ($useVersionFilter) { + $paramClone.Version = $versionFilter + } + $paramClone.Type = 'Module' $execute = $true if ($paramClone.Repository) { $paramClone.Repository = $paramClone.Repository | Where-Object { $_ -match '\*' -or - $_ -in (Get-PSFRepository -Type V2).Name + $_ -in (Get-PSFRepository -Type V3).Name } $execute = $paramClone.Repository -as [bool] } - if ($execute) { - Find-Module @paramClone | ConvertFrom-ModuleInfo + Find-PSResource @paramClone | ConvertFrom-ModuleInfo } } - #endregion V2 + #endregion V3 - #region V3 - if ($script:psget.V3 -and $Type -in 'All', 'V3') { + #region V2 + if ($script:psget.V2 -and $Type -in 'All', 'V2') { $paramClone = $param.Clone() - $paramClone += $PSBoundParameters | ConvertTo-PSFHashtable -Include AllowPrerelease -Remap @{ - AllowPrerelease = 'Prerelease' - } - if ($useVersionFilter) { - $paramClone.Version = $versionFilter + $paramClone += $PSBoundParameters | ConvertTo-PSFHashtable -Include AllVersions, AllowPrerelease + if ($Version) { + if ($convertedVersion.Required) { $paramClone.RequiredVersion = $convertedVersion.Required } + if ($convertedVersion.Minimum) { $paramClone.MinimumVersion = $convertedVersion.Minimum } + if ($convertedVersion.Maximum) { $paramClone.MaximumVersion = $convertedVersion.Maximum } } - $paramClone.Type = 'Module' $execute = $true if ($paramClone.Repository) { $paramClone.Repository = $paramClone.Repository | Where-Object { $_ -match '\*' -or - $_ -in (Get-PSFRepository -Type V3).Name + $_ -in (Get-PSFRepository -Type V2).Name } $execute = $paramClone.Repository -as [bool] } + if ($execute) { - Find-PSResource @paramClone | ConvertFrom-ModuleInfo + $paramClone = $paramClone | ConvertTo-PSFHashtable -ReferenceCommand Find-Module + if ($AllowPrerelease -and $paramClone.Keys -notcontains 'AllowPrerelease') { + Write-PSFMessage -Level Warning -String 'Find-PSFModule.AllowPrerease.NotSupported' -Once 'OldPSGetV2_Prerelease' + } + Find-Module @paramClone | ConvertFrom-ModuleInfo } } - #endregion V3 + #endregion V2 } } \ No newline at end of file diff --git a/PSFramework.NuGet/functions/PowerShellGet/Install-PSFPowerShellGet.ps1 b/PSFramework.NuGet/functions/PowerShellGet/Install-PSFPowerShellGet.ps1 index 8e6711e..a6a207e 100644 --- a/PSFramework.NuGet/functions/PowerShellGet/Install-PSFPowerShellGet.ps1 +++ b/PSFramework.NuGet/functions/PowerShellGet/Install-PSFPowerShellGet.ps1 @@ -43,6 +43,12 @@ .PARAMETER NotInternal Do not use the internally provided PowerShellGet module versions. This REQUIRES you to either provide the module data via -SourcePath or to have live online access. + + .PARAMETER UserMode + Deploy the resource into user paths, rather than computer-wide. + This allows bootstrapping _without_ requiring elevation and is usually only needed on the local computer. + This mode is automatically selected when deploying to the local computer and not running PowerShell "As Administrator". + Only applies to / affects Windows computers. .EXAMPLE PS C:\> Install-PSFPowerShell -Type V3Latest -ComputerName (Get-ADComputer -Filter * -SearchBase $myOU) @@ -69,7 +75,10 @@ $Offline, [switch] - $NotInternal + $NotInternal, + + [switch] + $UserMode ) begin { @@ -161,7 +170,7 @@ $actualConfiguration = Import-PSFPowerShellDataFile -Path (Join-Path -Path $rootPath -ChildPath 'modules.json') $data = @{ - Type = $Type + Type = $Type Config = $actualConfiguration } switch ($Type) { @@ -180,7 +189,9 @@ #region Actual Code $code = { param ( - $Data + $Data, + + $AsCurrentUser ) #region Functions @@ -233,14 +244,19 @@ #region V2 Bootstrap V2Binaries { if ($isOnWindows) { - if (-not (Test-Path -Path "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet")) { - $null = New-Item -Path "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet" -ItemType Directory -Force + $getRoot = "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet" + if ($AsCurrentUser) { $getRoot = "$env:LocalAppData\Microsoft\Windows\PowerShell\PowerShellGet" } + if (-not (Test-Path -Path $getRoot)) { + $null = New-Item -Path $getRoot -ItemType Directory -Force } - Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'NuGet.exe') -Destination "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet" -Force - if (-not (Test-Path -Path "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208")) { - $null = New-Item -Path "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208" -ItemType Directory -Force + Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'NuGet.exe') -Destination $getRoot -Force + + $nugetRoot = "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208" + if ($AsCurrentUser) { $nugetRoot = "$env:LOCALAPPDATA\PackageManagement\ProviderAssemblies\nuget\2.8.5.208"} + if (-not (Test-Path -Path $nugetRoot)) { + $null = New-Item -Path $nugetRoot -ItemType Directory -Force } - Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'Microsoft.PackageManagement.NuGetProvider.dll') -Destination "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208" -Force + Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'Microsoft.PackageManagement.NuGetProvider.dll') -Destination $nugetRoot -Force } else { Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'NuGet.exe') -Destination "$HOME/.config/powershell/powershellget" -Force @@ -251,6 +267,10 @@ #region V2 Latest V2Latest { $modulesFolder = "$env:ProgramFiles\WindowsPowerShell\modules" + if ($AsCurrentUser) { + $modulesFolder = $env:PSModulePath -split ';' | Microsoft.PowerShell.Core\Where-Object { $_ -match '\\Documents\\' } | Microsoft.PowerShell.Utility\Select-Object -First 1 + if (-not $modulesFolder) { $env:PSModulePath -split ';' | Microsoft.PowerShell.Utility\Select-Object -First 1 } + } if (-not $isOnWindows) { $modulesFolder = "/usr/local/share/powershell/Modules" } Install-ZipModule -Config $data.Config.PSGetV2 -ModulesFolder $modulesFolder -TempFolder $tempFolder @@ -261,6 +281,10 @@ #region V3 Latest V3Latest { $modulesFolder = "$env:ProgramFiles\WindowsPowerShell\modules" + if ($AsCurrentUser) { + $modulesFolder = $env:PSModulePath -split ';' | Microsoft.PowerShell.Core\Where-Object { $_ -match '\\Documents\\' } | Microsoft.PowerShell.Utility\Select-Object -First 1 + if (-not $modulesFolder) { $env:PSModulePath -split ';' | Microsoft.PowerShell.Utility\Select-Object -First 1 } + } if (-not $isOnWindows) { $modulesFolder = "/usr/local/share/powershell/Modules" } Install-ZipModule -Config $data.Config.PSGetV3 -ModulesFolder $modulesFolder -TempFolder $tempFolder @@ -285,6 +309,14 @@ $useInternal = $false } } + + $asCurrentUser = $UserMode.ToBool() + if (-not $asCurrentUser -and + ($env:COMPUTERNAME -eq $ComputerName) -and + (-not (Test-PSFPowerShell -Elevated)) + ) { + $asCurrentUser = $true + } #endregion Resolve Source Configuration } process { @@ -298,7 +330,7 @@ $binaries = Resolve-PowerShellGet -Type $typeEntry -Offline:$stayOffline -SourcePath $SourcePath -NotInternal:$useInternal # Execute Deployment - Invoke-PSFCommand -ComputerName $ComputerName -ScriptBlock $code -Credential $Credential -ArgumentList $binaries + Invoke-PSFCommand -ComputerName $ComputerName -ScriptBlock $code -Credential $Credential -ArgumentList $binaries, $asCurrentUser } } end { diff --git a/PSFramework.NuGet/functions/Resource/Publish-PSFResourceModule.ps1 b/PSFramework.NuGet/functions/Resource/Publish-PSFResourceModule.ps1 index d041650..2ffa03b 100644 --- a/PSFramework.NuGet/functions/Resource/Publish-PSFResourceModule.ps1 +++ b/PSFramework.NuGet/functions/Resource/Publish-PSFResourceModule.ps1 @@ -165,7 +165,8 @@ try { New-DummyModule -Path $stagingDirectory -Name $Name -Version $Version -RequiredModules $RequiredModules -Description $Description -Author $Author $resources = New-Item -Path $stagingDirectory -Name Resources -ItemType Directory -Force - $Path | Copy-Item -Destination $resources.FullName -Recurse -Force -Confirm:$false -WhatIf:$false + Copy-Item -LiteralPath $($Path) -Destination $resources.FullName -Recurse -Force -Confirm:$false -WhatIf:$false + ConvertTo-TransportFile -Path $resources.FullName Publish-PSFModule @publishParam -Path $stagingDirectory -ErrorAction Stop } diff --git a/PSFramework.NuGet/functions/Resource/Save-PSFResourceModule.ps1 b/PSFramework.NuGet/functions/Resource/Save-PSFResourceModule.ps1 index c5eb6ea..09c4671 100644 --- a/PSFramework.NuGet/functions/Resource/Save-PSFResourceModule.ps1 +++ b/PSFramework.NuGet/functions/Resource/Save-PSFResourceModule.ps1 @@ -88,6 +88,14 @@ [string[]] $Name, + [Parameter(Mandatory = $true, Position = 1)] + [PSFDirectory] + $Path, + + [PsfArgumentCompleter('PSFramework.NuGet.Repository')] + [string[]] + $Repository = ((Get-PSFrepository).Name | Sort-Object -Unique), + [Parameter(ParameterSetName = 'ByName')] [string] $Version, @@ -96,10 +104,6 @@ [switch] $Prerelease, - [Parameter(Mandatory = $true, Position = 1)] - [PSFDirectory] - $Path, - [switch] $SkipDependency, @@ -112,10 +116,6 @@ [PSCredential] $Credential, - [PsfArgumentCompleter('PSFramework.NuGet.Repository')] - [string[]] - $Repository = ((Get-PSFrepository).Name | Sort-Object -Unique), - [switch] $TrustRepository, @@ -152,6 +152,8 @@ continue } + ConvertFrom-TransportFile -Path $dataPath + foreach ($item in Get-ChildItem -LiteralPath $dataPath) { $targetPath = Join-Path -Path $pathEntry -ChildPath $item.Name if (-not $Force -and (Test-path -Path $targetPath)) { diff --git a/PSFramework.NuGet/internal/functions/Get/Publish/ConvertFrom-TransportFile.ps1 b/PSFramework.NuGet/internal/functions/Get/Publish/ConvertFrom-TransportFile.ps1 new file mode 100644 index 0000000..dbfd0db --- /dev/null +++ b/PSFramework.NuGet/internal/functions/Get/Publish/ConvertFrom-TransportFile.ps1 @@ -0,0 +1,34 @@ +function ConvertFrom-TransportFile { + <# + .SYNOPSIS + Unwraps a previously created transport file. + + .DESCRIPTION + Unwraps a previously created transport file. + These are created as part of the publishing step of resource modules, in order to ensure transport fidelity with PSResourceGet. + This command will expand the transport archive and remove the placeholder files previously created. + + .PARAMETER Path + The path to the Resources folder within the Resource Module being downloaded. + + .EXAMPLE + PS C:\> ConvertFrom-TransportFile -Path $dataPath + + Unwraps any transport file in the specified resources directory. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Path + ) + process { + $archivePath = Join-Path -Path $Path -ChildPath '___þþþ_transportplaceholder_þþþ___.zip' + if (-not (Test-Path -LiteralPath $archivePath)) { return } + + Expand-Archive -Path $archivePath -DestinationPath $Path + Remove-Item -LiteralPath $archivePath -Force + + Get-ChildItem -LiteralPath $Path -Recurse -Force | Where-Object Name -eq '___þþþ_transportplaceholder_þþþ___.txt' | Remove-Item + } +} \ No newline at end of file diff --git a/PSFramework.NuGet/internal/functions/Get/Publish/ConvertTo-TransportFile.ps1 b/PSFramework.NuGet/internal/functions/Get/Publish/ConvertTo-TransportFile.ps1 new file mode 100644 index 0000000..e175950 --- /dev/null +++ b/PSFramework.NuGet/internal/functions/Get/Publish/ConvertTo-TransportFile.ps1 @@ -0,0 +1,39 @@ +function ConvertTo-TransportFile { + <# + .SYNOPSIS + Wraps up the payload of a resoure module into a single archive. + + .DESCRIPTION + Wraps up the payload of a resoure module into a single archive. + This is unfortunately required to maintain content fidelity, due to errors in the PSResourceGet module. + Before creating the archive, we place a dummy file in every empty folder, to prevent it from being skipped. + + .PARAMETER Path + Path to the Resource folder, containing the files & folders to wrap up. + + .EXAMPLE + PS C:\> ConvertTo-TransportFile -Path $resourcePath + + Wraps up the specified payload into a single archive. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Path + ) + process { + $directories = Get-ChildItem -LiteralPath $Path -Recurse -Directory + foreach ($directory in $directories) { + $countChildren = $directory.GetFileSystemInfos('*', [System.IO.SearchOption]::TopDirectoryOnly).Count + if ($countChildren -gt 0) { continue } + + $null = New-Item -Path $directory.FullName -Name '___þþþ_transportplaceholder_þþþ___.txt' -ItemType File -Value 42 + } + + $archivePath = Join-Path -Path $Path -ChildPath '___þþþ_transportplaceholder_þþþ___.zip' + $items = Get-ChildItem -LiteralPath $Path + Compress-Archive -LiteralPath $items.FullName -DestinationPath $archivePath -Force + $items | Remove-Item -Recurse -Force + } +} \ No newline at end of file diff --git a/PSFramework.NuGet/internal/functions/Get/Save-StagingModule.ps1 b/PSFramework.NuGet/internal/functions/Get/Save-StagingModule.ps1 index 6625a30..3448bed 100644 --- a/PSFramework.NuGet/internal/functions/Get/Save-StagingModule.ps1 +++ b/PSFramework.NuGet/internal/functions/Get/Save-StagingModule.ps1 @@ -116,6 +116,9 @@ $tempDirectory = New-PSFTempDirectory -Name StagingSub -ModuleName PSFramework.NuGet $param = $Item.v2Param $actualParam = $param + $callSpecifics | ConvertTo-PSFHashtable -ReferenceCommand Save-Module + if ($param.AllowPrerelease -and $actualParam.Keys -notcontains 'AllowPrerelease') { + Write-PSFMessage -Level Warning -String 'Save-PSFModule.AllowPrerease.NotSupported' -Once 'OldPSGetV2_Prerelease' + } # 1) Save to temp folder try { Save-Module @actualParam -Path $tempDirectory } diff --git a/README.md b/README.md index d8dcd11..dd41f71 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,18 @@ So, here is a way to bootstrap your current console without requiring PowerShell iwr https://raw.githubusercontent.com/PowershellFrameworkCollective/PSFramework.NuGet/refs/heads/master/bootstrap.ps1 | iex ``` +> Update the Tooling + +This toolkit tries to help make module installation go smoothly with as little effort for you as possible. +However, it still uses the official Microsoft Modules to download and modules for maximum compatibility. +If some of the things you want to work still will not, you may need to update your PowerShellGet modules, which can be done with this line: + +```powershell +Install-PSFPowerShellGet -Type V2Binaries, V2Latest, V3Latest +``` + +After that line, start a new console and you should be up-to-date on all your tools needed. + ## Features ### Module Installation (Local or Remote) @@ -104,7 +116,7 @@ So lets fix this: ```powershell # Bootstrap Binaries for old Versions -Install-PSFPowerShellGet -Type V2Binaries -ComputerName $sessions +Install-PSFPowerShellGet -Type V2Binaries -ComputerName server1, server2, server3 # Install Latest V2 Install-PSFPowerShellGet -Type V2Latest -ComputerName $sessions