From 16b58f752f38a3be28ee8acba0ffecda60b23cd0 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 7 Oct 2021 21:46:52 -0700 Subject: [PATCH 1/9] add noclobber code --- src/code/InstallHelper.cs | 71 ++++++++++++++++++++++++++++------- src/code/InstallPSResource.cs | 8 +++- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 15998e30e..7d5cae053 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -45,6 +45,7 @@ internal class InstallHelper : PSCmdlet string _specifiedPath; bool _asNupkg; bool _includeXML; + List _pathsToSearch = new List(); public InstallHelper(bool updatePkg, bool savePkg, PSCmdlet cmdletPassedIn) { @@ -103,6 +104,18 @@ public void InstallPackages( _includeXML = includeXML; _pathsToInstallPkg = pathsToInstallPkg; + // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable) + // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations + // e.g.: + // ./InstallPackagePath1/PackageA + // ./InstallPackagePath1/PackageB + // ./InstallPackagePath2/PackageC + // ./InstallPackagePath3/PackageD + foreach (var path in _pathsToInstallPkg) + { + _pathsToSearch.AddRange(Utils.GetSubDirectories(path)); + } + // Go through the repositories and see which is the first repository to have the pkg version available ProcessRepositories(names, repository, _trustRepository, _credential); } @@ -218,20 +231,7 @@ public IEnumerable FilterByInstalledPkgs(IEnumerable _pathsToSearch = new List(); GetHelper getHelper = new GetHelper(_cmdletPassedIn); - // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable) - // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations - // e.g.: - // ./InstallPackagePath1/PackageA - // ./InstallPackagePath1/PackageB - // ./InstallPackagePath2/PackageC - // ./InstallPackagePath3/PackageD - foreach (var path in _pathsToInstallPkg) - { - _pathsToSearch.AddRange(Utils.GetSubDirectories(path)); - } - IEnumerable pkgsAlreadyInstalled = getHelper.FilterPkgPaths(pkgNames.ToArray(), _versionRange, _pathsToSearch); // If any pkg versions are already installed, write a message saying it is already installed and continue processing other pkg names @@ -404,6 +404,12 @@ private List InstallPackage(IEnumerable pkgsToInstall, s { continue; } + + // If NoClobber is specified, ensure command clobbering does not happen + if (_noClobber && !DetectClobber(tempDirNameVersion)) + { + continue; + } } // Delete the extra nupkg related files that are not needed and not part of the module/script @@ -535,6 +541,45 @@ private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string t return success; } + private bool DetectClobber(string tempDirNameVersion) + { + // Get installed modules, then get all possible paths + bool foundClobber = false; + GetHelper getHelper = new GetHelper(_cmdletPassedIn); + IEnumerable pkgsAlreadyInstalled = getHelper.FilterPkgPaths(new string[] { "*" }, VersionRange.All, _pathsToSearch); + + var PSGetModuleInfoXML = Path.Combine(tempDirNameVersion, "PSGetModuleInfo.xml"); + + PSResourceInfo.TryRead(PSGetModuleInfoXML, out PSResourceInfo psGetInfo, out string errorMsg); + if (!String.IsNullOrEmpty(errorMsg)) _cmdletPassedIn.WriteDebug(string.Format("Error reading from '{0}': '{1}'.", PSGetModuleInfoXML, errorMsg)); + + foreach (var pkg in pkgsAlreadyInstalled) + { + if (pkg.Includes.Command != null && pkg.Includes.Command.Any()) + { + // See if any of the commands in the pkg we're trying to install exist within a package that's already installed + // We can't use the PSResourceInfo object here because that doesn't have Includes info. We need to use the PSModuleInfo.xml file. + if (psGetInfo.Includes.Command.Any(command => pkg.Includes.Command.Contains(command))) + { + var duplicateCommands = psGetInfo.Includes.Command.Where(command => pkg.Includes.Command.Contains(command)).ToList(); + var exMessage = string.Format( + "The following commands are already available on this system: '{0}'. This module '{1}' may override the existing commands. If you still want to install this module '{1}', remove the -NoClobber parameter.", + String.Join(", ", duplicateCommands), psGetInfo.Name); + + var ex = new ArgumentException(exMessage); + var noClobberError = new ErrorRecord(ex, "CommandAlreadyExists", ErrorCategory.ResourceExists, null); + + _cmdletPassedIn.WriteError(noClobberError); + foundClobber = true; + + return foundClobber; + } + } + } + + return foundClobber; + } + private void CreateMetadataXMLFile(string dirNameVersion, string installPath, string repoName, PSResourceInfo pkg, bool isScript) { // Script will have a metadata file similar to: "TestScript_InstalledScriptInfo.xml" diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index 78e7ecd62..6710b9e7e 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -87,6 +87,12 @@ class InstallPSResource : PSCmdlet [Parameter(ParameterSetName = NameParameterSet)] public SwitchParameter AcceptLicense { get; set; } + /// + /// Prevents installing a package that contains cmdlets that already exist on the machine. + /// + [Parameter(ParameterSetName = NameParameterSet)] + public SwitchParameter NoClobber { get; set; } + #endregion #region Members @@ -177,7 +183,7 @@ protected override void ProcessRecord() reinstall: Reinstall, force: false, trustRepository: TrustRepository, - noClobber: false, + noClobber: NoClobber, credential: Credential, requiredResourceFile: null, requiredResourceJson: null, From 6e8ca71c9584b229e6ed8f569c1ff8ff1ef11204 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 11 Oct 2021 22:08:49 -0700 Subject: [PATCH 2/9] Allow both 'command' and 'cmdlet' clobbering to be detected --- src/code/InstallHelper.cs | 52 +++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 7d5cae053..c6f41adfb 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -406,7 +406,7 @@ private List InstallPackage(IEnumerable pkgsToInstall, s } // If NoClobber is specified, ensure command clobbering does not happen - if (_noClobber && !DetectClobber(tempDirNameVersion)) + if (_noClobber && !DetectClobber(p.Name, tempDirNameVersion, parsedMetadataHashtable)) { continue; } @@ -541,39 +541,49 @@ private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string t return success; } - private bool DetectClobber(string tempDirNameVersion) + private bool DetectClobber(string pkgName, string tempDirNameVersion, Hashtable parsedMetadataHashtable) { // Get installed modules, then get all possible paths bool foundClobber = false; GetHelper getHelper = new GetHelper(_cmdletPassedIn); IEnumerable pkgsAlreadyInstalled = getHelper.FilterPkgPaths(new string[] { "*" }, VersionRange.All, _pathsToSearch); - - var PSGetModuleInfoXML = Path.Combine(tempDirNameVersion, "PSGetModuleInfo.xml"); - - PSResourceInfo.TryRead(PSGetModuleInfoXML, out PSResourceInfo psGetInfo, out string errorMsg); - if (!String.IsNullOrEmpty(errorMsg)) _cmdletPassedIn.WriteDebug(string.Format("Error reading from '{0}': '{1}'.", PSGetModuleInfoXML, errorMsg)); + // user parsed metadata hash + List listOfCmdlets = new List(); + foreach (var cmdlet in parsedMetadataHashtable["CmdletsToExport"] as object[]) + { + listOfCmdlets.Add(cmdlet as string); + } foreach (var pkg in pkgsAlreadyInstalled) { + List duplicateCmdlets = new List(); + List duplicateCmds = new List(); + // See if any of the cmdlets or commands in the pkg we're trying to install exist within a package that's already installed + if (pkg.Includes.Cmdlet != null && pkg.Includes.Cmdlet.Any()) + { + duplicateCmdlets = listOfCmdlets.Where(cmdlet => pkg.Includes.Cmdlet.Contains(cmdlet)).ToList(); + + } if (pkg.Includes.Command != null && pkg.Includes.Command.Any()) { - // See if any of the commands in the pkg we're trying to install exist within a package that's already installed - // We can't use the PSResourceInfo object here because that doesn't have Includes info. We need to use the PSModuleInfo.xml file. - if (psGetInfo.Includes.Command.Any(command => pkg.Includes.Command.Contains(command))) - { - var duplicateCommands = psGetInfo.Includes.Command.Where(command => pkg.Includes.Command.Contains(command)).ToList(); - var exMessage = string.Format( - "The following commands are already available on this system: '{0}'. This module '{1}' may override the existing commands. If you still want to install this module '{1}', remove the -NoClobber parameter.", - String.Join(", ", duplicateCommands), psGetInfo.Name); + duplicateCmds = listOfCmdlets.Where(commands => pkg.Includes.Command.Contains(commands)).ToList(); + } + if (duplicateCmdlets.Any() || duplicateCmds.Any()) + { + + duplicateCmdlets.AddRange(duplicateCmds); - var ex = new ArgumentException(exMessage); - var noClobberError = new ErrorRecord(ex, "CommandAlreadyExists", ErrorCategory.ResourceExists, null); + var errMessage = string.Format( + "The following commands are already available on this system: '{0}'. This module '{1}' may override the existing commands. If you still want to install this module '{1}', remove the -NoClobber parameter.", + String.Join(", ", duplicateCmdlets), pkgName); - _cmdletPassedIn.WriteError(noClobberError); - foundClobber = true; + var ex = new ArgumentException(errMessage); + var noClobberError = new ErrorRecord(ex, "CommandAlreadyExists", ErrorCategory.ResourceExists, null); - return foundClobber; - } + _cmdletPassedIn.WriteError(noClobberError); + foundClobber = true; + + return foundClobber; } } From 3784e2a5eaede2d922a895c9b343b836edcdfd88 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Mon, 11 Oct 2021 23:37:06 -0700 Subject: [PATCH 3/9] Add test --- test/InstallPSResource.Tests.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index 4a61f1549..83ca0eefb 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -243,6 +243,16 @@ Describe 'Test Install-PSResource for Module' { $res = Get-Module "TestModule" -ListAvailable $res | Should -BeNullOrEmpty } + + It "Install module using -NoClobber, should throw clobber error and not install the module" { + Install-PSResource -Name "ClobberTestModule1" -Repository $TestGalleryName + + $res = Get-Module "ClobberTestModule1" -ListAvailable + $res.Name | Should -Be "ClobberTestModule1" + + Install-PSResource -Name "ClobberTestModule2" -Repository $TestGalleryName -NoClobber -ErrorAction SilentlyContinue + $Error[0].FullyQualifiedErrorId | Should -be "CommandAlreadyExists,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } } <# Temporarily commented until -Tag is implemented for this Describe block From cc6a6827a2acc40d8af6400e264bf29b1231c95b Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 13 Oct 2021 13:36:22 -0700 Subject: [PATCH 4/9] Update src/code/InstallHelper.cs Co-authored-by: Paul Higinbotham --- src/code/InstallHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 79baa0671..262bf1ab9 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -554,7 +554,8 @@ private bool DetectClobber(string pkgName, string tempDirNameVersion, Hashtable List listOfCmdlets = new List(); foreach (var cmdlet in parsedMetadataHashtable["CmdletsToExport"] as object[]) { - listOfCmdlets.Add(cmdlet as string); + listOfCmdlets.Add(cmdletName as string); + } foreach (var pkg in pkgsAlreadyInstalled) From eabd0041d14623d245b4633ed811f4cdd5adb02e Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 13 Oct 2021 13:36:33 -0700 Subject: [PATCH 5/9] Update src/code/InstallHelper.cs Co-authored-by: Paul Higinbotham --- src/code/InstallHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 262bf1ab9..08f0e08f2 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -552,7 +552,7 @@ private bool DetectClobber(string pkgName, string tempDirNameVersion, Hashtable IEnumerable pkgsAlreadyInstalled = getHelper.FilterPkgPaths(new string[] { "*" }, VersionRange.All, _pathsToSearch); // user parsed metadata hash List listOfCmdlets = new List(); - foreach (var cmdlet in parsedMetadataHashtable["CmdletsToExport"] as object[]) + foreach (var cmdletName in parsedMetadataHashtable["CmdletsToExport"] as object[]) { listOfCmdlets.Add(cmdletName as string); From 51c0761d94c1246f586e1cdc251beeb788535dc6 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 4 Nov 2021 13:24:05 -0700 Subject: [PATCH 6/9] Make string compare case insensitive --- src/code/InstallHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index eaa1898dc..543600bda 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -573,7 +573,7 @@ private bool DetectClobber(string pkgName, string tempDirNameVersion, Hashtable } if (pkg.Includes.Command != null && pkg.Includes.Command.Any()) { - duplicateCmds = listOfCmdlets.Where(commands => pkg.Includes.Command.Contains(commands)).ToList(); + duplicateCmds = listOfCmdlets.Where(commands => pkg.Includes.Command.Contains(commands, StringComparer.InvariantCultureIgnoreCase)).ToList(); } if (duplicateCmdlets.Any() || duplicateCmds.Any()) { From fbaa2127e82eeb4b10349af81c2ea5511f2dc4de Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 4 Nov 2021 15:03:06 -0700 Subject: [PATCH 7/9] Fix code that was accidentally removed --- src/code/InstallHelper.cs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 543600bda..ba325f834 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -37,6 +37,26 @@ internal class InstallHelper : PSCmdlet private CancellationToken _cancellationToken; private readonly bool _savePkg; private readonly PSCmdlet _cmdletPassedIn; + private List _pathsToInstallPkg; + private VersionRange _versionRange; + private bool _prerelease; + private bool _acceptLicense; + private bool _quiet; + private bool _reinstall; + private bool _force; + private bool _trustRepository; + private PSCredential _credential; + private string _specifiedPath; + private bool _asNupkg; + private bool _includeXML; + private bool _noClobber; + List _pathsToSearch; + + #endregion + + #region Public methods + + public InstallHelper(bool savePkg, PSCmdlet cmdletPassedIn) { CancellationTokenSource source = new CancellationTokenSource(); _cancellationToken = source.Token; @@ -86,6 +106,9 @@ public void InstallPackages( _includeXML = includeXML; _pathsToInstallPkg = pathsToInstallPkg; + // Create list of installation paths to search. + _pathsToSearch = new List(); + // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable) // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations // e.g.: @@ -202,7 +225,6 @@ private IEnumerable FilterByInstalledPkgs(IEnumerable _pathsToSearch = new List(); - GetHelper getHelper = new GetHelper(_cmdletPassedIn); // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable) // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations // e.g.: @@ -221,6 +243,7 @@ private IEnumerable FilterByInstalledPkgs(IEnumerable pkgsAlreadyInstalled = getHelper.GetPackagesFromPath( name: filteredPackages.Keys.ToArray(), @@ -552,7 +575,7 @@ private bool DetectClobber(string pkgName, string tempDirNameVersion, Hashtable // Get installed modules, then get all possible paths bool foundClobber = false; GetHelper getHelper = new GetHelper(_cmdletPassedIn); - IEnumerable pkgsAlreadyInstalled = getHelper.FilterPkgPaths(new string[] { "*" }, VersionRange.All, _pathsToSearch); + IEnumerable pkgsAlreadyInstalled = getHelper.GetPackagesFromPath(new string[] { "*" }, VersionRange.All, _pathsToSearch); // user parsed metadata hash List listOfCmdlets = new List(); foreach (var cmdletName in parsedMetadataHashtable["CmdletsToExport"] as object[]) From a539f2054b25dd1eee28317b4931fca242dc7b10 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 4 Nov 2021 15:16:44 -0700 Subject: [PATCH 8/9] Clean up code --- src/code/InstallHelper.cs | 8 ++++---- src/code/InstallPSResource.cs | 14 +++++++++----- src/code/SavePSResource.cs | 4 +--- src/code/UpdatePSResource.cs | 4 +--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index ba325f834..554b90413 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -74,16 +74,15 @@ public void InstallPackages( bool reinstall, bool force, bool trustRepository, + bool noClobber, PSCredential credential, - string requiredResourceJson, - Hashtable requiredResourceHash, string specifiedPath, bool asNupkg, bool includeXML, List pathsToInstallPkg) { _cmdletPassedIn.WriteVerbose(string.Format("Parameters passed in >>> Name: '{0}'; Version: '{1}'; Prerelease: '{2}'; Repository: '{3}'; " + - "AcceptLicense: '{4}'; Quiet: '{5}'; Reinstall: '{6}'; TrustRepository: '{7}';", + "AcceptLicense: '{4}'; Quiet: '{5}'; Reinstall: '{6}'; TrustRepository: '{7}'; NoClobber: '{8}';", string.Join(",", names), (versionRange != null ? versionRange.OriginalString : string.Empty), prerelease.ToString(), @@ -91,7 +90,8 @@ public void InstallPackages( acceptLicense.ToString(), quiet.ToString(), reinstall.ToString(), - trustRepository.ToString())); + trustRepository.ToString(), + noClobber.ToString())); _versionRange = versionRange; _prerelease = prerelease; diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index cc2021d92..ea5e6812c 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -70,7 +70,7 @@ class InstallPSResource : PSCmdlet [Parameter(ParameterSetName = NameParameterSet)] [Parameter(ParameterSetName = InputObjectParameterSet)] public SwitchParameter TrustRepository { get; set; } - + /// /// Overwrites a previously installed resource with the same name and version. /// @@ -92,6 +92,12 @@ class InstallPSResource : PSCmdlet [Parameter(ParameterSetName = InputObjectParameterSet)] public SwitchParameter AcceptLicense { get; set; } + /// + /// Prevents installing a package that contains cmdlets that already exist on the machine. + /// + [Parameter(ParameterSetName = NameParameterSet)] + public SwitchParameter NoClobber { get; set; } + /// /// Used for pipeline input. /// @@ -235,11 +241,9 @@ private void ProcessInstallHelper(InstallHelper installHelper, string[] pkgNames reinstall: Reinstall, force: false, trustRepository: TrustRepository, + noClobber: NoClobber, credential: Credential, - requiredResourceFile: null, - requiredResourceJson: null, - requiredResourceHash: null, - specifiedPath: null, + specifiedPath: null, asNupkg: false, includeXML: true, pathsToInstallPkg: _pathsToInstallPkg); diff --git a/src/code/SavePSResource.cs b/src/code/SavePSResource.cs index dd8bf7d84..5449333af 100644 --- a/src/code/SavePSResource.cs +++ b/src/code/SavePSResource.cs @@ -236,9 +236,7 @@ private void ProcessSaveHelper(InstallHelper installHelper, string[] pkgNames, b force: false, trustRepository: TrustRepository, credential: Credential, - requiredResourceFile: null, - requiredResourceJson: null, - requiredResourceHash: null, + noClobber: false, specifiedPath: _path, asNupkg: false, includeXML: false, diff --git a/src/code/UpdatePSResource.cs b/src/code/UpdatePSResource.cs index fe2eb92a4..7158bc4c6 100644 --- a/src/code/UpdatePSResource.cs +++ b/src/code/UpdatePSResource.cs @@ -154,9 +154,7 @@ protected override void ProcessRecord() force: Force, trustRepository: TrustRepository, credential: Credential, - requiredResourceFile: null, - requiredResourceJson: null, - requiredResourceHash: null, + noClobber: false, specifiedPath: null, asNupkg: false, includeXML: true, From fd16dadfa18e8ec4896d6bf56bc02c38f92cd48b Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Thu, 4 Nov 2021 21:20:12 -0700 Subject: [PATCH 9/9] Sett noclobber global variable --- src/code/InstallHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 554b90413..d92d9c7d6 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -100,6 +100,7 @@ public void InstallPackages( _reinstall = reinstall; _force = force; _trustRepository = trustRepository; + _noClobber = noClobber; _credential = credential; _specifiedPath = specifiedPath; _asNupkg = asNupkg;