From eb2443b1c47d2fbec9e6a591e7fee27b6e41c343 Mon Sep 17 00:00:00 2001 From: Afroz Mohammed Date: Fri, 24 Jan 2025 13:27:23 -0800 Subject: [PATCH 1/3] Fix #1777: Fixes bug with nuspec dependency version range when versions are specified in RequiredModules section Fixes bug with generated nuspec dependency version range when RequiredVersion,MaxiumumVersion and ModuleVersion are specified in RequiredModules section --- src/code/PublishHelper.cs | 57 ++++-- .../CompressPSResource.Tests.ps1 | 177 ++++++++++++++++++ 2 files changed, 220 insertions(+), 14 deletions(-) diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 66daea84e..944a8b7f4 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -1122,16 +1122,38 @@ private string CreateNuspec( if (requiredModules != null) { XmlElement dependenciesElement = doc.CreateElement("dependencies", nameSpaceUri); - foreach (string dependencyName in requiredModules.Keys) { XmlElement element = doc.CreateElement("dependency", nameSpaceUri); - element.SetAttribute("id", dependencyName); - string dependencyVersion = requiredModules[dependencyName].ToString(); - if (!string.IsNullOrEmpty(dependencyVersion)) + + var requiredModulesVersionInfo = (Hashtable)requiredModules[dependencyName]; + string versionRange = ""; + if (requiredModulesVersionInfo.ContainsKey("RequiredVersion")) + { + // For RequiredVersion, use exact version notation [x.x.x] + string requiredModulesVersion = requiredModulesVersionInfo["RequiredVersion"].ToString(); + versionRange = $"[{requiredModulesVersion}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion") && requiredModulesVersionInfo.ContainsKey("MaximumVersion")) + { + // Version range when both min and max specified: [min,max] + versionRange = $"[{requiredModulesVersionInfo["ModuleVersion"]}, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion")) + { + // Only min specified: min (which means ≥ min) + versionRange = requiredModulesVersionInfo["ModuleVersion"].ToString(); + } + else if (requiredModulesVersionInfo.ContainsKey("MaximumVersion")) { - element.SetAttribute("version", requiredModules[dependencyName].ToString()); + // Only max specified: (, max] + versionRange = $"(, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } + + if (!string.IsNullOrEmpty(versionRange)) + { + element.SetAttribute("version", versionRange); } dependenciesElement.AppendChild(element); @@ -1173,19 +1195,26 @@ private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash) if (LanguagePrimitives.TryConvertTo(reqModule, out Hashtable moduleHash)) { string moduleName = moduleHash["ModuleName"] as string; - - if (moduleHash.ContainsKey("ModuleVersion")) + var versionInfo = new Hashtable(); + + // RequiredVersion cannot be used with ModuleVersion or MaximumVersion + if (moduleHash.ContainsKey("RequiredVersion")) { - dependenciesHash.Add(moduleName, moduleHash["ModuleVersion"]); + versionInfo["RequiredVersion"] = moduleHash["RequiredVersion"].ToString(); } - else if (moduleHash.ContainsKey("RequiredVersion")) - { - dependenciesHash.Add(moduleName, moduleHash["RequiredVersion"]); - } - else + else { - dependenciesHash.Add(moduleName, string.Empty); + // ModuleVersion and MaximumVersion can be used together + if (moduleHash.ContainsKey("ModuleVersion")) + { + versionInfo["ModuleVersion"] = moduleHash["ModuleVersion"].ToString(); + } + if (moduleHash.ContainsKey("MaximumVersion")) + { + versionInfo["MaximumVersion"] = moduleHash["MaximumVersion"].ToString(); + } } + dependenciesHash.Add(moduleName, versionInfo); } else if (LanguagePrimitives.TryConvertTo(reqModule, out string moduleName)) { diff --git a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 index 7d9fe9770..2ef0e5b05 100644 --- a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 @@ -42,6 +42,39 @@ function CreateTestModule '@ | Out-File -FilePath $moduleSrc } +function CompressExpandRetrieveNuspec +{ + param( + [string]$PublishModuleBase, + [string]$PublishModuleName, + [string]$ModuleVersion, + [string]$RepositoryPath, + [string]$ModuleBasePath, + [string]$TestDrive, + [object[]]$RequiredModules, + [switch]$SkipModuleManifestValidate + ) + + $testFile = Join-Path -Path "TestSubDirectory" -ChildPath "TestSubDirFile.ps1" + $null = New-ModuleManifest -Path (Join-Path -Path $PublishModuleBase -ChildPath "$PublishModuleName.psd1") -ModuleVersion $version -Description "$PublishModuleName module" -RequiredModules $RequiredModules + $null = New-Item -Path (Join-Path -Path $PublishModuleBase -ChildPath $testFile) -Force + + $null = Compress-PSResource -Path $PublishModuleBase -DestinationPath $repositoryPath -SkipModuleManifestValidate:$SkipModuleManifestValidate + + # Must change .nupkg to .zip so that Expand-Archive can work on Windows PowerShell + $nupkgPath = Join-Path -Path $RepositoryPath -ChildPath "$PublishModuleName.$version.nupkg" + $zipPath = Join-Path -Path $RepositoryPath -ChildPath "$PublishModuleName.$version.zip" + Rename-Item -Path $nupkgPath -NewName $zipPath + $unzippedPath = Join-Path -Path $TestDrive -ChildPath "$PublishModuleName" + $null = New-Item $unzippedPath -Itemtype directory -Force + $null = Expand-Archive -Path $zipPath -DestinationPath $unzippedPath + + $nuspecPath = Join-Path -Path $unzippedPath -ChildPath "$PublishModuleName.nuspec" + $nuspecxml = [xml](Get-Content $nuspecPath) + $null = Remove-Item $unzippedPath -Force -Recurse + return $nuspecxml +} + Describe "Test Compress-PSResource" -tags 'CI' { BeforeAll { Get-NewPSResourceRepositoryFile @@ -218,6 +251,150 @@ Describe "Test Compress-PSResource" -tags 'CI' { $fileInfoObject.Name | Should -Be "$script:PublishModuleName.$version.nupkg" } + It "Compress-PSResource creates nuspec dependecy version range when RequiredVersion is in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'RequiredVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + # removing spaces as the nuget packaging is formatting the version range and adding spaces even when the original nuspec file doesn't have spaces. + # e.g (,2.0.0] is being formatted to (, 2.0.0] + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '[2.0.0]' + } + + It "Compress-PSResource creates nuspec dependecy version range when ModuleVersion is in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '2.0.0' + } + + It "Compress-PSResource creates nuspec dependecy version range when MaximumVersion is in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'MaximumVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '(,2.0.0]' + } + + It "Compress-PSResource creates nuspec dependecy version range when ModuleVersion and MaximumVersion are in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '1.0.0' + 'MaximumVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '[1.0.0,2.0.0]' + } + + It "Compress-PSResource creates nuspec dependecy version range when there are multiple modules in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModuleRequiredVersion' + 'GUID' = (New-Guid).Guid + 'RequiredVersion' = '1.0.0' + }, + @{ + 'ModuleName' = 'PSGetTestRequiredModuleModuleVersion' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '2.0.0' + }, + @{ + 'ModuleName' = 'PSGetTestRequiredModuleMaximumVersion' + 'GUID' = (New-Guid).Guid + 'MaximumVersion' = '3.0.0' + }, + @{ + 'ModuleName' = 'PSGetTestRequiredModuleModuleAndMaximumVersion' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '4.0.0' + 'MaximumVersion' = '5.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + foreach ($dependency in $nuspecxml.package.metadata.dependencies.dependency) { + switch ($dependency.id) { + "PSGetTestRequiredModuleRequiredVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '[1.0.0]' + } + "PSGetTestRequiredModuleModuleVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '2.0.0' + } + "PSGetTestRequiredModuleMaximumVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '(,3.0.0]' + } + "PSGetTestRequiredModuleModuleAndMaximumVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '[4.0.0,5.0.0]' + } + } + } + } + <# Test for Signing the nupkg. Signing doesn't work It "Compressed Module is able to be signed with a certificate" { $version = "1.0.0" From c8221215441752ec9ac6883438ac72d237699c5a Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 4 Mar 2025 17:53:12 -0500 Subject: [PATCH 2/3] only convert Version info to hashtable if its not an empty string --- src/code/PublishHelper.cs | 57 +++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 944a8b7f4..4cbfb0f4a 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -1126,38 +1126,43 @@ private string CreateNuspec( { XmlElement element = doc.CreateElement("dependency", nameSpaceUri); element.SetAttribute("id", dependencyName); - - var requiredModulesVersionInfo = (Hashtable)requiredModules[dependencyName]; - string versionRange = ""; - if (requiredModulesVersionInfo.ContainsKey("RequiredVersion")) + + string dependencyVersion = requiredModules[dependencyName].ToString(); + if (!string.IsNullOrEmpty(dependencyVersion)) { - // For RequiredVersion, use exact version notation [x.x.x] - string requiredModulesVersion = requiredModulesVersionInfo["RequiredVersion"].ToString(); - versionRange = $"[{requiredModulesVersion}]"; - } - else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion") && requiredModulesVersionInfo.ContainsKey("MaximumVersion")) - { - // Version range when both min and max specified: [min,max] - versionRange = $"[{requiredModulesVersionInfo["ModuleVersion"]}, {requiredModulesVersionInfo["MaximumVersion"]}]"; - } - else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion")) - { - // Only min specified: min (which means ≥ min) - versionRange = requiredModulesVersionInfo["ModuleVersion"].ToString(); - } - else if (requiredModulesVersionInfo.ContainsKey("MaximumVersion")) - { - // Only max specified: (, max] - versionRange = $"(, {requiredModulesVersionInfo["MaximumVersion"]}]"; - } + var requiredModulesVersionInfo = (Hashtable)requiredModules[dependencyName]; + string versionRange = String.Empty; + if (requiredModulesVersionInfo.ContainsKey("RequiredVersion")) + { + // For RequiredVersion, use exact version notation [x.x.x] + string requiredModulesVersion = requiredModulesVersionInfo["RequiredVersion"].ToString(); + versionRange = $"[{requiredModulesVersion}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion") && requiredModulesVersionInfo.ContainsKey("MaximumVersion")) + { + // Version range when both min and max specified: [min,max] + versionRange = $"[{requiredModulesVersionInfo["ModuleVersion"]}, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion")) + { + // Only min specified: min (which means ≥ min) + versionRange = requiredModulesVersionInfo["ModuleVersion"].ToString(); + } + else if (requiredModulesVersionInfo.ContainsKey("MaximumVersion")) + { + // Only max specified: (, max] + versionRange = $"(, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } - if (!string.IsNullOrEmpty(versionRange)) - { - element.SetAttribute("version", versionRange); + if (!string.IsNullOrEmpty(versionRange)) + { + element.SetAttribute("version", versionRange); + } } dependenciesElement.AppendChild(element); } + metadataElement.AppendChild(dependenciesElement); } From 4e674783916cc5aa1eaf245c596697b5f2688317 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 4 Mar 2025 18:51:18 -0500 Subject: [PATCH 3/3] delete unzippedPath in test after it's written to or subsequent Expand-Archive calls to that path will fail --- test/PublishPSResourceTests/CompressPSResource.Tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 index 2ef0e5b05..28f74a742 100644 --- a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 @@ -185,6 +185,7 @@ Describe "Test Compress-PSResource" -tags 'CI' { Expand-Archive -Path $zipPath -DestinationPath $unzippedPath Test-Path -Path (Join-Path -Path $unzippedPath -ChildPath $testFile) | Should -Be $True + $null = Remove-Item $unzippedPath -Force -Recurse } It "Compresses a script" {