diff --git a/src/code/UninstallPSResource.cs b/src/code/UninstallPSResource.cs index 9bf85fa7f..9e0ed90a5 100644 --- a/src/code/UninstallPSResource.cs +++ b/src/code/UninstallPSResource.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Management.Automation; +using System.Collections; namespace Microsoft.PowerShell.PSResourceGet.Cmdlets { @@ -179,7 +180,7 @@ private bool UninstallPkgHelper(out List errRecords) { var successfullyUninstalled = false; GetHelper getHelper = new GetHelper(this); - List dirsToDelete = getHelper.FilterPkgPathsByName(Name, _pathsToSearch); + List dirsToDelete = getHelper.FilterPkgPathsByName(Name, _pathsToSearch); int totalDirs = dirsToDelete.Count; errRecords = new List(); @@ -268,17 +269,17 @@ private bool UninstallModuleHelper(string pkgPath, string pkgName, out ErrorReco { errRecord = null; var successfullyUninstalledPkg = false; + DirectoryInfo dir = new DirectoryInfo(pkgPath); + Uri uri = new Uri(dir.FullName); + string version = uri.Segments.Last(); // if -SkipDependencyCheck is not specified and the pkg is a dependency for another package, // an error will be written and we return false - if (!SkipDependencyCheck && CheckIfDependency(pkgName, out errRecord)) + if (!SkipDependencyCheck && CheckIfDependency(pkgName, version, out errRecord)) { return false; } - DirectoryInfo dir = new DirectoryInfo(pkgPath); - Uri uri = new Uri(dir.FullName); - string version = uri.Segments.Last(); if (!ShouldProcess(string.Format("Uninstall resource '{0}', version '{1}', from path '{2}'", pkgName, version, dir.FullName))) { WriteVerbose("ShouldProcess is set to false."); @@ -374,8 +375,9 @@ private bool UninstallScriptHelper(string pkgPath, string pkgName, out ErrorReco return successfullyUninstalledPkg; } - private bool CheckIfDependency(string pkgName, out ErrorRecord errorRecord) + private bool CheckIfDependency(string pkgName, string version, out ErrorRecord errorRecord) { + // Checking if a specific package version is a dependency anywhere // this is a primitive implementation // TODO: implement a dependencies database for querying dependency info // cannot uninstall a module if another module is dependent on it @@ -389,24 +391,23 @@ private bool CheckIfDependency(string pkgName, out ErrorRecord errorRecord) } // _dependentModules is a collection of PSModuleInfo objects that contain a property listing module dependencies, "RequiredModules". - // RequiredModules is collection of PSModuleInfo objects that need to be iterated through to see if any of them are the pkg we're trying to uninstall // If we anything from the final call gets returned, there is a dependency on this pkg. - HashSet uniquePackageNames = new(); + List parentPackages = new(); errorRecord = null; try { for (int i = 0; i < _dependentModules.Count; i++) { - var reqModules = _dependentModules[i].RequiredModules; + var reqModule = _dependentModules[i].RequiredModules; - for (int j = 0; j < reqModules.Count; j++) + for (int j = 0; j < reqModule.Count; j++) { - if (reqModules[j].Name.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)) + if (reqModule[j].Name.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)) { - uniquePackageNames.Add(_dependentModules[i].Name); + parentPackages.Add(_dependentModules[i]); } } } @@ -420,17 +421,58 @@ private bool CheckIfDependency(string pkgName, out ErrorRecord errorRecord) null); } - if (uniquePackageNames.Count > 0) + bool dependencyExists = false; + List parentsOfDependency = new List(); + Version systemVersion = null; + for (int i = 0; i < parentPackages.Count; i++) { - var strUniquePkgNames = string.Join(",", uniquePackageNames.ToArray()); + var parentPkg = parentPackages[i] as PSModuleInfo; + + for (int j = 0; j < parentPkg.RequiredModules.Count; j++) + { + var pkgToUninstall = parentPkg.RequiredModules[j] as PSModuleInfo; + if (string.Equals(pkgToUninstall.Name, pkgName, StringComparison.InvariantCultureIgnoreCase)) { + // then check verison + if (pkgToUninstall.Version == null) + { + // Any version works as a dependency, so only one version needs to be available. + _pwsh ??= System.Management.Automation.PowerShell.Create(); + _pwsh.Commands.Clear(); + + // Get all versions of the package to be uninstalled + Collection pkgVersions = _pwsh.AddCommand("Microsoft.PowerShell.Core\\Get-Module").AddParameters( + new Hashtable() { + { "Name", pkgName }, + { "ListAvailable", true } + }).Invoke(); + + if (pkgVersions.Count == 1) + { + parentsOfDependency.Add(parentPkg.Name); + dependencyExists = true; + } + } + else { + if (System.Version.TryParse(version, out systemVersion) && pkgToUninstall.Version.CompareTo(systemVersion) == 0) + { + // The required version OR module version is the version we're attempting to uninstall. + parentsOfDependency.Add(parentPkg.Name); + dependencyExists = true; + } + } + } + } + } + if (dependencyExists) + { + string versionMsg = systemVersion == null ? string.Empty : $"version '{systemVersion}' of "; errorRecord = new ErrorRecord( new PSInvalidOperationException( - $"Cannot uninstall '{pkgName}'. This package depends on the following package(s): '{strUniquePkgNames}'. If you would still like to uninstall, re-run the command with -SkipDependencyCheck."), - - "UninstallPSResourcePackageIsaDependency", + $"Cannot uninstall {versionMsg}'{pkgName}'. This package is a dependency for: '{string.Join(", ", parentsOfDependency)}'. If you would still like to uninstall, re-run the command with -SkipDependencyCheck."), + "UninstallPSResourcePackageIsADependency", ErrorCategory.InvalidOperation, - null); + this); return true; } diff --git a/test/UninstallPSResourceTests/UninstallPSResource.Tests.ps1 b/test/UninstallPSResourceTests/UninstallPSResource.Tests.ps1 index 7231b734f..146020660 100644 --- a/test/UninstallPSResourceTests/UninstallPSResource.Tests.ps1 +++ b/test/UninstallPSResourceTests/UninstallPSResource.Tests.ps1 @@ -22,6 +22,7 @@ Describe 'Test Uninstall-PSResource for Modules' -tags 'CI' { AfterEach { Uninstall-PSResource -Name $testModuleName -Version "*" -ErrorAction SilentlyContinue -SkipDependencyCheck + Uninstall-PSResource -Name "test_module" -Version "*" -ErrorAction SilentlyContinue -SkipDependencyCheck } AfterAll { @@ -32,7 +33,7 @@ Describe 'Test Uninstall-PSResource for Modules' -tags 'CI' { Uninstall-PSResource -name $testModuleName -SkipDependencyCheck Get-InstalledPSResource $testModuleName | Should -BeNullOrEmpty } - +<# $testCases = @{Name="Test?Module"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"}, @{Name="Test[Module"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"} @@ -211,7 +212,7 @@ Describe 'Test Uninstall-PSResource for Modules' -tags 'CI' { $stableVersionPkgs = $res | Where-Object {$_.IsPrerelease -ne $true} $stableVersionPkgs.Count | Should -Be 2 } - +#> It "uninstall all prerelease versions (which satisfy the range) when -Version range and -Prerelease parameter is specified" { Uninstall-PSResource -Name $testModuleName -Version "*" -SkipDependencyCheck Install-PSResource -Name $testModuleName -Version "2.5.0-beta" -Repository $PSGalleryName -TrustRepository @@ -250,14 +251,46 @@ Describe 'Test Uninstall-PSResource for Modules' -tags 'CI' { } It "Do not Uninstall module that is a dependency for another module" { - $null = Install-PSResource "test_module" -Repository $PSGalleryName -TrustRepository -WarningAction SilentlyContinue + Install-PSResource "test_module" -Repository $PSGalleryName -TrustRepository -Version "3.0.0" -Reinstall + + $pkg = Get-InstalledPSResource "test_module" + write-host "$($pkg.Name)" + write-host "$($pkg.Version)" + + $pkg = Get-InstalledPSResource "RequiredModule1" + $pkg | Should -Not -Be $null + write-host "$($pkg.Name)" + write-host "$($pkg.Version)" + + Uninstall-PSResource -Name "RequiredModule1" -ErrorVariable ev -ErrorAction SilentlyContinue -Verbose - Uninstall-PSResource -Name "RequiredModule1" -ErrorVariable ev -ErrorAction SilentlyContinue + write-host $ev $pkg = Get-InstalledPSResource "RequiredModule1" $pkg | Should -Not -Be $null - $ev.FullyQualifiedErrorId | Should -BeExactly 'UninstallPSResourcePackageIsaDependency,Microsoft.PowerShell.PSResourceGet.Cmdlets.UninstallPSResource' + $ev.FullyQualifiedErrorId | Should -BeExactly 'UninstallPSResourcePackageIsADependency,Microsoft.PowerShell.PSResourceGet.Cmdlets.UninstallPSResource' + } + + It "Do not uninstall specific module version that is dependency for another module" { + $null = Install-PSResource "test_module" -Repository $PSGalleryName -TrustRepository -SkipDependencyCheck -Version "1.0.0.0" + $null = Install-PSResource "test_module" -Repository $PSGalleryName -TrustRepository -SkipDependencyCheck -Version "3.0.0.0" + $null = Install-PSResource "test_module" -Repository $PSGalleryName -TrustRepository -SkipDependencyCheck -Version "5.0.0.0" + $null = Install-PSResource "ModuleWithDependency" -Repository $PSGalleryName -TrustRepository -SkipDependencyCheck + + $pkg = Get-InstalledPSResource "test_module" + $pkg | Should -Not -Be $null + $pkg.Count | Should -BeGreaterOrEqual 3 + + write-host "************* LOOK HERE************************************" + write-host "$($pkg.Count)" + + Uninstall-PSResource -Name "test_module" -Version "*" -Verbose + + $pkg = Get-InstalledPSResource "test_module" + $pkg | Should -Not -Be $null + $pkg.Count | Should -Be 1 + $ev.FullyQualifiedErrorId | Should -BeExactly 'UninstallPSResourcePackageIsADependency,Microsoft.PowerShell.PSResourceGet.Cmdlets.UninstallPSResource' } It "Uninstall module that is a dependency for another module using -SkipDependencyCheck" {