diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 7edd67461..d92d9c7d6 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -49,6 +49,8 @@ internal class InstallHelper : PSCmdlet private string _specifiedPath; private bool _asNupkg; private bool _includeXML; + private bool _noClobber; + List _pathsToSearch; #endregion @@ -72,17 +74,15 @@ public void InstallPackages( bool reinstall, bool force, bool trustRepository, + bool noClobber, PSCredential credential, - string requiredResourceFile, - 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(), @@ -90,7 +90,8 @@ public void InstallPackages( acceptLicense.ToString(), quiet.ToString(), reinstall.ToString(), - trustRepository.ToString())); + trustRepository.ToString(), + noClobber.ToString())); _versionRange = versionRange; _prerelease = prerelease; @@ -99,12 +100,28 @@ public void InstallPackages( _reinstall = reinstall; _force = force; _trustRepository = trustRepository; + _noClobber = noClobber; _credential = credential; _specifiedPath = specifiedPath; _asNupkg = asNupkg; _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.: + // ./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); } @@ -209,7 +226,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.: @@ -228,6 +244,7 @@ private IEnumerable FilterByInstalledPkgs(IEnumerable pkgsAlreadyInstalled = getHelper.GetPackagesFromPath( name: filteredPackages.Keys.ToArray(), @@ -410,6 +427,12 @@ private List InstallPackage(IEnumerable pkgsToInstall, s { continue; } + + // If NoClobber is specified, ensure command clobbering does not happen + if (_noClobber && !DetectClobber(p.Name, tempDirNameVersion, parsedMetadataHashtable)) + { + continue; + } } // Delete the extra nupkg related files that are not needed and not part of the module/script @@ -548,6 +571,56 @@ private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string t return success; } + 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.GetPackagesFromPath(new string[] { "*" }, VersionRange.All, _pathsToSearch); + // user parsed metadata hash + List listOfCmdlets = new List(); + foreach (var cmdletName in parsedMetadataHashtable["CmdletsToExport"] as object[]) + { + listOfCmdlets.Add(cmdletName 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()) + { + duplicateCmds = listOfCmdlets.Where(commands => pkg.Includes.Command.Contains(commands, StringComparer.InvariantCultureIgnoreCase)).ToList(); + } + if (duplicateCmdlets.Any() || duplicateCmds.Any()) + { + + duplicateCmdlets.AddRange(duplicateCmds); + + 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); + + var ex = new ArgumentException(errMessage); + 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 isModule) { // Script will have a metadata file similar to: "TestScript_InstalledScriptInfo.xml" 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, diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index 8762290a9..9e5f56fd7 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -279,7 +279,16 @@ Describe 'Test Install-PSResource for Module' { $res.Path.Contains("Modules") | Should -Be $true } - It "Install PSResourceInfo object piped in" { + 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" + } + It "Install PSResourceInfo object piped in" { Find-PSResource -Name $testModuleName -Version "1.1.0.0" -Repository $TestGalleryName | Install-PSResource $res = Get-InstalledPSResource -Name $testModuleName $res.Name | Should -Be $testModuleName