Skip to content

Save-PSResource doesn't install the correct version. #1657

Closed

Description

@ADD-Izan-Garcia

Prerequisites

Steps to reproduce

I am experiencing a problem with Save-PSResource. When installing a specific version of a module from a NuGet server, it doesn't take the one specified.
Steps to reproduce:

  1. Have one powershell module in a NuGet server with two different versions published, like '2024.5.20.1' and 2024.5.20.12'
  2. Download the module with the cmdlet:
Save-PSResource -Name ExampleModule -Version [2024.5.20.1] -Repository ExampleRepository -Path  C:\Users\ExampleUser\Documents -SkipDependencyCheck

Expected behavior

ExampleModule installed in:
C:\Users\ExampleUser\Documents\ExampleModule\2024.5.20.1\ExampleModule.psd1
C:\Users\ExampleUser\Documents\ExampleModule\2024.5.20.1\ExampleModule.psm1 

Inside the ExampleModule.psd1, the field ModuleVersion with the correct version, in this case '2024.5.20.1'

Actual behavior

ExampleModule installed in:
C:\Users\ExampleUser\Documents\ExampleModule\2024.5.20.1\ExampleModule.psd1
C:\Users\ExampleUser\Documents\ExampleModule\2024.5.20.1\ExampleModule.psm1 

Inside the ExampleModule.psd1, the field ModuleVersion with an incorrect version, in this case '2024.5.20.12'

Error details

No response

Environment data

PSVersion 7.4.2
PSEdition Core
GitCommitId 7.4.2
OS Microsoft Windows 10.0.20348
Platfor Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0....}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0

Visuals

No response

Activity

  1. 237dmitry commented on May 20, 2024

    @237dmitry

    I could not reproduce:

    $ Save-PSResource -Name PSWSMan -Version 2.3.0 -Path ./Downloads/ -IncludeXml
    $ tree -d ./Downloads/PSWSMan/
    ./Downloads/PSWSMan/
    └── 2.3.0
        ├── bin
        │   ├── glibc-1.0
        │   ├── glibc-1.1
        │   ├── glibc-3
        │   ├── macOS-1.1
        │   ├── macOS-3
        │   ├── musl-1.1
        │   └── musl-3
        └── en-US
    
  2. ADD-Izan-Garcia commented on May 20, 2024

    @ADD-Izan-Garcia
    Author

    Its not the same scenario. I think the problem is that instead of taking the version '2024.5.20.1' as exact version, is taking it as minimum version. That's why then the version '2024.5.20.12' is taken.

  3. 237dmitry commented on May 20, 2024

    @237dmitry

    Its not the same scenario.

    A did not find modules with similar versions:

    $ Save-PSResource -Name PSWriteColor -Version 0.7 -Path ./Downloads/ -IncludeXml
    $ Save-PSResource -Name PSWriteColor -Version 0.71 -Path ./Downloads/ -IncludeXml
    $ (dir ./Downloads/PSWriteColor/).Name
    0.7
    0.71
  4. mklement0 commented on May 20, 2024

    @mklement0

    I also cannot reproduce the problem, at least with the PSWriteColor module.

    The -Version parameter is explicitly documented as being version-exact (no need for [...] enclosure), and that to get the minimum-compatible-version behavior you'd have to use something like '[1.0.0,]' (note the comma).

  5. mklement0 commented on May 20, 2024

    @mklement0

    On second thought:

    • The Package Versioning rules seem to be based on semver version numbers, which are limited to three components, with an optional suffix.

    • Therefore, 2024.5.20.1 is not a valid semver ([semver]) version number (but is a valid [version] number).

    I wonder if the problem is related to that, and how such invalid-as-semver version numbers are de facto / should be handled.

  6. o-l-a-v commented on Jul 24, 2024

    @o-l-a-v
    Contributor

    Example module from PowerShell Gallery with NuGet versioning (four digits vs. semver three, ref: https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#where-nugetversion-diverges-from-semantic-versioning):

    Older than latest version:

    Using `-Version '1.0.1.314'` works as expected:
    PS > $DebugPreference = 'Continue'; Save-PSResource -Repository 'PSGallery' -TrustRepository -SkipDependencyCheck -Name 'Intune.USB.Creator' -Version '1.0.1.314' -Path 'C:\Users\olavb\Desktop\Testin'
    DEBUG: In SavePSResource::ProcessSaveHelper()
    DEBUG: In InstallHelper::BeginInstallPackages()
    DEBUG: Parameters passed in >>> Name: 'Intune.USB.Creator'; VersionRange: '1.0.1.314'; NuGetVersion: '1.0.1.314'; VersionType: 'SpecificVersion'; Version: '1.0.1.314'; Prerelease: 'False'; Repository: 'PSGallery'; AcceptLicense: 'True'; Quiet: 'False'; Reinstall: 'True'; TrustRepository: 'True'; NoClobber: 'False'; AsNupkg: 'False'; IncludeXml 'False'; SavePackage 'True'; TemporaryPath ''; SkipDependencyCheck: 'True'; AuthenticodeCheck: 'False'; PathsToInstallPkg: 'C:\Users\olavb\Desktop\Testin'; Scope ''
    DEBUG: In InstallHelper::ProcessRepositories()
    DEBUG: In InstallHelper::InstallPackages()
    DEBUG: In InstallHelper::InstallPackage()
    DEBUG: In V2ServerAPICalls::FindVersion()
    DEBUG: In V2ServerAPICalls::HttpRequestCall()
    DEBUG: Request url is 'https://www.powershellgallery.com/api/v2/FindPackagesById()?$filter=Id+eq+%27Intune.USB.Creator%27+and+NormalizedVersion+eq+%271.0.1.314%27&$inlinecount=allpages&id=%27Intune.USB.Creator%27'
    DEBUG: Count from response is '1'
    DEBUG: In V2ServerAPICalls::InstallVersion()
    DEBUG: In V2ServerAPICalls::HttpRequestCallForContent()
    DEBUG: Request url is 'https://www.powershellgallery.com/api/v2/package/Intune.USB.Creator/1.0.1.314'
    DEBUG: In InstallHelper::TryInstallToTempPath()
    DEBUG: In InstallHelper::DeleteExtraneousFiles()
    DEBUG: Deleting 'C:\Users\olavb\AppData\Local\Temp\3df2f957-c6ce-4b30-895b-232483e64984\intune.usb.creator\1.0.1.314\Intune.USB.Creator.nuspec'
    DEBUG: Deleting 'C:\Users\olavb\AppData\Local\Temp\3df2f957-c6ce-4b30-895b-232483e64984\intune.usb.creator\1.0.1.314\[Content_Types].xml'
    DEBUG: Deleting 'C:\Users\olavb\AppData\Local\Temp\3df2f957-c6ce-4b30-895b-232483e64984\intune.usb.creator\1.0.1.314\_rels'
    DEBUG: Deleting 'C:\Users\olavb\AppData\Local\Temp\3df2f957-c6ce-4b30-895b-232483e64984\intune.usb.creator\1.0.1.314\package'
    DEBUG: In InstallHelper::TryMoveInstallContent()
    DEBUG: In InstallHelper::MoveFilesIntoInstallPath()
    PS>
    Using `-Version [1.0.1.314]` worked too in my testing (as in, did install that exact version):
    PS > $DebugPreference = 'Continue'; Save-PSResource -Repository 'PSGallery' -TrustRepository -SkipDependencyCheck -Name 'Intune.USB.Creator' -Version [1.0.1.314] -Path 'C:\Users\olavb\Desktop\Testin'
    DEBUG: In SavePSResource::ProcessSaveHelper()
    DEBUG: In InstallHelper::BeginInstallPackages()
    DEBUG: Parameters passed in >>> Name: 'Intune.USB.Creator'; VersionRange: '[1.0.1.314]'; NuGetVersion: ''; VersionType: 'VersionRange'; Version: '[1.0.1.314]'; Prerelease: 'False'; Repository: 'PSGallery'; AcceptLicense: 'True'; Quiet: 'False'; Reinstall: 'True'; TrustRepository: 'True'; NoClobber: 'False'; AsNupkg: 'False'; IncludeXml 'False'; SavePackage 'True'; TemporaryPath ''; SkipDependencyCheck: 'True'; AuthenticodeCheck: 'False'; PathsToInstallPkg: 'C:\Users\olavb\Desktop\Testin'; Scope ''
    DEBUG: In InstallHelper::ProcessRepositories()
    DEBUG: In InstallHelper::InstallPackages()
    DEBUG: In InstallHelper::InstallPackage()
    DEBUG: In V2ServerAPICalls::FindVersionGlobbing()
    DEBUG: In V2ServerAPICalls::FindVersionGlobbing()
    DEBUG: In V2ServerAPICalls::HttpRequestCall()
    DEBUG: Request url is 'https://www.powershellgallery.com/api/v2/FindPackagesById()?$filter=NormalizedVersion+ge+%271.0.1.314%27+and+NormalizedVersion+le+%271.0.1.314%27+and+IsPrerelease+eq+false+and+Id+eq+%27Intune.USB.Creator%27&$inlinecount=allpages&$skip=0&$orderby=NormalizedVersion+desc&id=%27Intune.USB.Creator%27'
    DEBUG: In V2ServerAPICalls::InstallVersion()
    DEBUG: In V2ServerAPICalls::HttpRequestCallForContent()
    DEBUG: Request url is 'https://www.powershellgallery.com/api/v2/package/Intune.USB.Creator/1.0.1.314'
    DEBUG: In InstallHelper::TryInstallToTempPath()
    DEBUG: In InstallHelper::DeleteExtraneousFiles()
    DEBUG: Deleting 'C:\Users\olavb\AppData\Local\Temp\7b6a0df7-cdec-4637-9554-5c390d37609a\intune.usb.creator\1.0.1.314\Intune.USB.Creator.nuspec'
    DEBUG: Deleting 'C:\Users\olavb\AppData\Local\Temp\7b6a0df7-cdec-4637-9554-5c390d37609a\intune.usb.creator\1.0.1.314\[Content_Types].xml'
    DEBUG: Deleting 'C:\Users\olavb\AppData\Local\Temp\7b6a0df7-cdec-4637-9554-5c390d37609a\intune.usb.creator\1.0.1.314\_rels'
    DEBUG: Deleting 'C:\Users\olavb\AppData\Local\Temp\7b6a0df7-cdec-4637-9554-5c390d37609a\intune.usb.creator\1.0.1.314\package'
    DEBUG: In InstallHelper::TryMoveInstallContent()
    DEBUG: In InstallHelper::MoveFilesIntoInstallPath()
    PS >

    Not an issue? Might be an API issue with whatever backend OP used? I used PSResourceGet v1.0.5 against PSGallery.

  7. et1975 commented on Aug 19, 2026

    @et1975
    Contributor

    I believe the root cause is in the NuGet V3 download path rather than version-range parsing.

    FindVersionHelper compares parsed NuGetVersion values exactly, but InstallHelper selects the packageContent URL using a substring comparison:

    https://github.com/PowerShell/PSResourceGet/blob/3f0d2e5be761f9a7677a5162d6200447733c1460/src/code/V3ServerAPICalls.cs\#L918-L928

    if (response.Contains(version.ToNormalizedString()))
    {
        pkgContentUrl = response;
        break;
    }

    The asynchronous path contains the same comparison:

    https://github.com/PowerShell/PSResourceGet/blob/3f0d2e5be761f9a7677a5162d6200447733c1460/src/code/V3ServerAPICalls.cs\#L1002-L1012

    This allows prefix collisions. For example, when versions 1.2.3 and 1.2.30 exist, the packageContent URL for 1.2.30 also contains the requested normalized version string 1.2.3. Because responses are version-ordered and the loop stops at the first match, Save-PSResource -Version 1.2.3 can download the 1.2.30 payload while still writing it under the requested 1.2.3 destination.

    This also explains why exact range syntax does not prevent the problem: by this point the specification has already been parsed into the same normalized NuGetVersion, and the final selection is still substring-based.

    The download selection should compare the registration entry’s parsed version to the requested NuGetVersion, as FindVersionHelper does, rather than searching for the version text anywhere in the packageContent URL. Both the synchronous and asynchronous paths need the same change and regression coverage for prefix-adjacent versions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions