From 5df1e3b90701115d5978a7f937844a68b424b3fc Mon Sep 17 00:00:00 2001 From: anamnavi Date: Thu, 9 Apr 2026 17:23:42 -0400 Subject: [PATCH 1/3] Ignore dependency listed in ExternalModuleDependencies from RequiredModules --- src/code/FindHelper.cs | 23 +++++-- src/code/InstallHelper.cs | 134 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 144 insertions(+), 13 deletions(-) diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 388be9090..98fce65cd 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -1024,7 +1024,8 @@ private IEnumerable SearchByNames(ServerApiCall currentServer, R foreach (PSResourceInfo currentPkg in parentPkgs) { _cmdletPassedIn.WriteDebug($"Finding dependency packages for '{currentPkg.Name}'"); - foreach (PSResourceInfo pkgDep in FindDependencyPackages(currentServer, currentResponseUtil, currentPkg, repository)) + string[] emptyExternalModuleDependencies = new string[0]; + foreach (PSResourceInfo pkgDep in FindDependencyPackages(currentServer, currentResponseUtil, currentPkg, emptyExternalModuleDependencies, repository)) { yield return pkgDep; } @@ -1100,6 +1101,7 @@ internal IEnumerable FindDependencyPackages( ServerApiCall currentServer, ResponseUtil currentResponseUtil, PSResourceInfo currentPkg, + string[] externalModuleDependencies, PSRepositoryInfo repository) { if (currentPkg.Dependencies.Length > 0) @@ -1108,6 +1110,13 @@ internal IEnumerable FindDependencyPackages( { PSResourceInfo depPkg = null; + if (externalModuleDependencies.Contains(dep.Name, StringComparer.OrdinalIgnoreCase)) + { + _cmdletPassedIn.WriteVerbose($"Dependency '{dep.Name}' is listed as an external module dependency, skipping search for this dependency."); // TODO improve message to let user know they may need to install + continue; + } + + string[] emptyExternalModuleDependencies = new string[0]; if (dep.VersionRange.Equals(VersionRange.All)) { FindResults responses = currentServer.FindName(dep.Name, includePrerelease: true, _type, out ErrorRecord errRecord); @@ -1153,7 +1162,7 @@ internal IEnumerable FindDependencyPackages( if (!_packagesFound.ContainsKey(depPkg.Name)) { - foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository)) + foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, emptyExternalModuleDependencies, repository)) { yield return depRes; } @@ -1164,7 +1173,7 @@ internal IEnumerable FindDependencyPackages( // _packagesFound has depPkg.name in it, but the version is not the same if (!pkgVersions.Contains(FormatPkgVersionString(depPkg))) { - foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository)) + foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, emptyExternalModuleDependencies, repository)) { yield return depRes; } @@ -1217,7 +1226,7 @@ internal IEnumerable FindDependencyPackages( if (!_packagesFound.ContainsKey(depPkg.Name)) { - foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository)) + foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, emptyExternalModuleDependencies, repository)) { yield return depRes; } @@ -1228,7 +1237,7 @@ internal IEnumerable FindDependencyPackages( // _packagesFound has depPkg.name in it, but the version is not the same if (!pkgVersions.Contains(FormatPkgVersionString(depPkg))) { - foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository)) + foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, emptyExternalModuleDependencies, repository)) { yield return depRes; } @@ -1299,7 +1308,7 @@ internal IEnumerable FindDependencyPackages( if (!_packagesFound.ContainsKey(depPkg.Name)) { - foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository)) + foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, emptyExternalModuleDependencies, repository)) { yield return depRes; } @@ -1310,7 +1319,7 @@ internal IEnumerable FindDependencyPackages( // _packagesFound has depPkg.name in it, but the version is not the same if (!pkgVersions.Contains(FormatPkgVersionString(depPkg))) { - foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository)) + foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, emptyExternalModuleDependencies, repository)) { yield return depRes; } diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 0616cf040..075b960cf 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -558,6 +558,7 @@ private List InstallPackages( Hashtable parentPkgInfo = packagesHash[parentPackage] as Hashtable; PSResourceInfo parentPkgObj = parentPkgInfo["psResourceInfoPkg"] as PSResourceInfo; + string[] externalModuleDependencies = parentPkgInfo["externalModuleDependencies"] as string[]; if (!skipDependencyCheck) { @@ -565,7 +566,7 @@ private List InstallPackages( if (parentPkgObj.Dependencies.Length > 0) { bool depFindFailed = false; - foreach (PSResourceInfo depPkg in findHelper.FindDependencyPackages(currentServer, currentResponseUtil, parentPkgObj, repository)) + foreach (PSResourceInfo depPkg in findHelper.FindDependencyPackages(currentServer, currentResponseUtil, parentPkgObj, externalModuleDependencies, repository)) { if (depPkg == null) { @@ -824,7 +825,8 @@ private Hashtable BeginPackageInstall( { "tempDirNameVersionPath", tempInstallPath }, { "pkgVersion", "" }, { "scriptPath", "" }, - { "installPath", "" } + { "installPath", "" }, + { "externalModuleDependencies", Utils.EmptyStrArray } }); } } @@ -840,7 +842,8 @@ private Hashtable BeginPackageInstall( { "tempDirNameVersionPath", tempInstallPath }, { "pkgVersion", "" }, { "scriptPath", "" }, - { "installPath", "" } + { "installPath", "" }, + { "externalModuleDependencies", Utils.EmptyStrArray } }); } } @@ -933,6 +936,7 @@ private bool TryInstallToTempPath( _cmdletPassedIn.WriteDebug("In InstallHelper::TryInstallToTempPath()"); error = null; updatedPackagesHash = packagesHash; + string[] externalModuleDependencies = Utils.EmptyStrArray; try { var pathToFile = Path.Combine(tempInstallPath, $"{pkgName}.{normalizedPkgVersion}.zip"); @@ -1004,6 +1008,11 @@ private bool TryInstallToTempPath( return false; } + if (!RetrieveExternalModuleDependenciesForModule(pkgName, parsedMetadataHashtable, out externalModuleDependencies, out error)) + { + return false; + } + // Accept License verification if (!CallAcceptLicense(pkgToInstall, moduleManifest, tempInstallPath, pkgVersion, out error)) { @@ -1022,7 +1031,6 @@ private bool TryInstallToTempPath( { installPath = _pathsToInstallPkg.Find(path => path.EndsWith("Scripts", StringComparison.InvariantCultureIgnoreCase)); - // is script if (!PSScriptFileInfo.TryTestPSScriptFileInfo( scriptFileInfoPath: scriptPath, parsedScript: out PSScriptFileInfo scriptToInstall, @@ -1042,6 +1050,8 @@ private bool TryInstallToTempPath( return false; } + + externalModuleDependencies = scriptToInstall.ScriptMetadataComment.ExternalModuleDependencies; } else { @@ -1075,7 +1085,8 @@ private bool TryInstallToTempPath( { "tempDirNameVersionPath", tempDirNameVersion }, { "pkgVersion", pkgVersion }, { "scriptPath", scriptPath }, - { "installPath", installPath } + { "installPath", installPath }, + { "externalModuleDependencies", externalModuleDependencies } }); } @@ -1111,6 +1122,7 @@ private bool TrySaveNupkgToTempPath( _cmdletPassedIn.WriteDebug("In InstallHelper::TrySaveNupkgToTempPath()"); error = null; updatedPackagesHash = packagesHash; + string[] externalModuleDependencies = Utils.EmptyStrArray; try { @@ -1120,6 +1132,85 @@ private bool TrySaveNupkgToTempPath( responseStream.CopyTo(fs); fs.Close(); + var pkgVersion = pkgToInstall.Version.ToString(); + var tempDirNameVersion = Path.Combine(tempInstallPath, pkgName, pkgVersion); + Directory.CreateDirectory(tempDirNameVersion); + + if (!TryExtractToDirectory(pathToFile, tempDirNameVersion, out error)) + { + return false; + } + + var moduleManifest = Path.Combine(tempDirNameVersion, pkgName + PSDataFileExt); + var scriptPath = Path.Combine(tempDirNameVersion, pkgName + PSScriptFileExt); + + bool isModule = File.Exists(moduleManifest); + bool isScript = File.Exists(scriptPath); + + if (!isModule && !isScript) + { + scriptPath = ""; + } + + if (isModule) + { + if (!File.Exists(moduleManifest)) + { + error = new ErrorRecord( + new ArgumentException("Package '{pkgName}' could not be installed: Module manifest file: {moduleManifest} does not exist. This is not a valid PowerShell module."), + "PSDataFileNotExistError", + ErrorCategory.ReadError, + _cmdletPassedIn); + + return false; + } + + if (!Utils.TryReadManifestFile( + manifestFilePath: moduleManifest, + manifestInfo: out Hashtable parsedMetadataHashtable, + error: out Exception manifestReadError)) + { + error = new ErrorRecord( + manifestReadError, + "ManifestFileReadParseError", + ErrorCategory.ReadError, + _cmdletPassedIn); + + return false; + } + + if (!RetrieveExternalModuleDependenciesForModule(pkgName, parsedMetadataHashtable, out externalModuleDependencies, out error)) + { + return false; + } + } + else if(isScript) + { + if (!PSScriptFileInfo.TryTestPSScriptFileInfo( + scriptFileInfoPath: scriptPath, + parsedScript: out PSScriptFileInfo scriptToInstall, + out ErrorRecord[] parseScriptFileErrors, + out string[] _)) + { + foreach (ErrorRecord parseError in parseScriptFileErrors) + { + _cmdletPassedIn.WriteError(parseError); + } + + error = new ErrorRecord( + new InvalidOperationException($"PSScriptFile could not be parsed"), + "PSScriptParseError", + ErrorCategory.ReadError, + _cmdletPassedIn); + + return false; + } + + externalModuleDependencies = scriptToInstall.ScriptMetadataComment.ExternalModuleDependencies; + } + + DeleteExtraneousFiles(pkgName, tempDirNameVersion); + string installPath = _pathsToInstallPkg.First(); if (_includeXml) { @@ -1140,7 +1231,8 @@ private bool TrySaveNupkgToTempPath( { "tempDirNameVersionPath", tempInstallPath }, { "pkgVersion", "" }, { "scriptPath", "" }, - { "installPath", installPath } + { "installPath", installPath }, + { "externalModuleDependencies", externalModuleDependencies } }); } @@ -1531,6 +1623,36 @@ private void DeleteExtraneousFiles(string packageName, string dirNameVersion) } } + private bool RetrieveExternalModuleDependenciesForModule(string pkgName, Hashtable moduleMetadata, out string[] externalModuleDependencies, out ErrorRecord error) + { + error = null; + externalModuleDependencies = Utils.EmptyStrArray; + List externalModuleDependenciesForPkg = new List(); + + Hashtable privateData = moduleMetadata.ContainsKey("PrivateData") ? moduleMetadata["PrivateData"] as Hashtable : new Hashtable(StringComparer.InvariantCultureIgnoreCase); + Hashtable psData = privateData.ContainsKey("PSData") ? privateData["PSData"] as Hashtable : new Hashtable(StringComparer.InvariantCultureIgnoreCase); + object[] externalModDepObjects = psData.ContainsKey("ExternalModuleDependencies") ? psData["ExternalModuleDependencies"] as object[] : new object[0]; + foreach (var dep in externalModDepObjects) + { + string dependencyName = dep as string; + if (dependencyName.Contains("=")) + { + error = new ErrorRecord( + new ArgumentException($"Package '{pkgName}' could not be installed: ExternalModuleDependencies should only contain module names, not other metadata. Invalid entry: '{dependencyName}'"), + "ExternalModuleDependencyInvalidEntry", + ErrorCategory.ReadError, + _cmdletPassedIn); + + return false; + } + + externalModuleDependenciesForPkg.Add(dependencyName); + } + + externalModuleDependencies = externalModuleDependenciesForPkg.ToArray(); + return true; + } + #endregion } } From ec6480fd29aee5848f13f4d76a558c4768296fad Mon Sep 17 00:00:00 2001 From: anamnavi Date: Thu, 9 Apr 2026 17:33:39 -0400 Subject: [PATCH 2/3] Clean up verbose message --- src/code/FindHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 98fce65cd..5b5f62d6e 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -1112,7 +1112,7 @@ internal IEnumerable FindDependencyPackages( if (externalModuleDependencies.Contains(dep.Name, StringComparer.OrdinalIgnoreCase)) { - _cmdletPassedIn.WriteVerbose($"Dependency '{dep.Name}' is listed as an external module dependency, skipping search for this dependency."); // TODO improve message to let user know they may need to install + _cmdletPassedIn.WriteVerbose($"Dependency '{dep.Name}' is listed as an external module dependency, skipping search/install for this dependency."); continue; } From 04244054330ef9942230b6da7e3fb3524d9a4c1f Mon Sep 17 00:00:00 2001 From: anamnavi Date: Thu, 9 Apr 2026 19:34:00 -0400 Subject: [PATCH 3/3] Add tests for Save-PSResource and Install-PSResource --- src/code/InstallHelper.cs | 25 +++++++++++-------- .../InstallPSResourceV2Server.Tests.ps1 | 10 +++++++- .../SavePSResourceV2.Tests.ps1 | 16 ++++++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 075b960cf..07e96062e 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -1632,21 +1632,24 @@ private bool RetrieveExternalModuleDependenciesForModule(string pkgName, Hashtab Hashtable privateData = moduleMetadata.ContainsKey("PrivateData") ? moduleMetadata["PrivateData"] as Hashtable : new Hashtable(StringComparer.InvariantCultureIgnoreCase); Hashtable psData = privateData.ContainsKey("PSData") ? privateData["PSData"] as Hashtable : new Hashtable(StringComparer.InvariantCultureIgnoreCase); object[] externalModDepObjects = psData.ContainsKey("ExternalModuleDependencies") ? psData["ExternalModuleDependencies"] as object[] : new object[0]; - foreach (var dep in externalModDepObjects) + if (externalModDepObjects != null) { - string dependencyName = dep as string; - if (dependencyName.Contains("=")) + foreach (var dep in externalModDepObjects) { - error = new ErrorRecord( - new ArgumentException($"Package '{pkgName}' could not be installed: ExternalModuleDependencies should only contain module names, not other metadata. Invalid entry: '{dependencyName}'"), - "ExternalModuleDependencyInvalidEntry", - ErrorCategory.ReadError, - _cmdletPassedIn); + string dependencyName = dep as string; + if (dependencyName.Contains("=")) + { + error = new ErrorRecord( + new ArgumentException($"Package '{pkgName}' could not be installed: ExternalModuleDependencies should only contain module names, not other metadata. Invalid entry: '{dependencyName}'"), + "ExternalModuleDependencyInvalidEntry", + ErrorCategory.ReadError, + _cmdletPassedIn); - return false; - } + return false; + } - externalModuleDependenciesForPkg.Add(dependencyName); + externalModuleDependenciesForPkg.Add(dependencyName); + } } externalModuleDependencies = externalModuleDependenciesForPkg.ToArray(); diff --git a/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 index ee35c3396..306193e26 100644 --- a/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 @@ -646,7 +646,15 @@ Describe 'Test Install-PSResource for V2 Server scenarios' -tags 'CI' { $depRes = Get-InstalledPSResource $depPkgName1, $depPkgName2 $depRes.Name | Should -Contain $depPkgName1 $depRes.Name | Should -Contain $depPkgName2 - } + } + + It "Install resource and dependency, while skipping dependency that is listed as external module dependency" { + $testParentModule = "test_module_ext_dep" + $requiredDependency = "test_module10" + $res = Install-PSresource -Name "test_module_ext_dep" -Repository $PSGalleryName -TrustRepository -PassThru + $res.Name | Should -Contain $testParentModule + $res.Name | Should -Contain $requiredDependency + } } Describe 'Test Install-PSResource for V2 Server scenarios' -tags 'ManualValidationOnly' { diff --git a/test/SavePSResourceTests/SavePSResourceV2.Tests.ps1 b/test/SavePSResourceTests/SavePSResourceV2.Tests.ps1 index 4b0269d82..b27bc3124 100644 --- a/test/SavePSResourceTests/SavePSResourceV2.Tests.ps1 +++ b/test/SavePSResourceTests/SavePSResourceV2.Tests.ps1 @@ -214,4 +214,20 @@ Describe 'Test HTTP Save-PSResource for V2 Server Protocol' -tags 'CI' { $err.Count | Should -BeGreaterThan 0 $err[0].FullyQualifiedErrorId | Should -BeExactly 'ErrorFilteringNamesForUnsupportedWildcards,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource' } + + It "Save resource and dependency, while skipping dependency that is listed as external module dependency" { + $testParentModule = "test_module_ext_dep" + $requiredDependency = "test_module10" + $res = Save-PSresource -Name "test_module_ext_dep" -Repository $PSGalleryName -Path $SaveDir -TrustRepository -PassThru + $res.Name | Should -Contain $testParentModule + $res.Name | Should -Contain $requiredDependency + } + + It "Save resource and dependency, as .nupkg, while skipping dependency that is listed as external module dependency" { + $testParentModule = "test_module_ext_dep" + $requiredDependency = "test_module10" + $res = Save-PSresource -Name "test_module_ext_dep" -Repository $PSGalleryName -AsNupkg -Path $SaveDir -TrustRepository -PassThru + $res.Name | Should -Contain $testParentModule + $res.Name | Should -Contain $requiredDependency + } }