Skip to content

Commit ea90802

Browse files
fix(publish): 🐛 honor ExternalModuleDependencies from PSData during dependency checks
- Read ExternalModuleDependencies from both top-level metadata and PrivateData.PSData - Normalize dependency lookup to case-insensitive hashtable keys - Add publish regression test for module manifests with external dependencies
1 parent 73b90e3 commit ea90802

2 files changed

Lines changed: 157 additions & 8 deletions

File tree

src/code/PublishHelper.cs

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,7 +1195,7 @@ private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash)
11951195
// c. A string array of module names
11961196
// d. A single string module name
11971197

1198-
var dependenciesHash = new Hashtable();
1198+
var dependenciesHash = new Hashtable(StringComparer.OrdinalIgnoreCase);
11991199
foreach (var reqModule in requiredModules)
12001200
{
12011201
if (LanguagePrimitives.TryConvertTo<Hashtable>(reqModule, out Hashtable moduleHash))
@@ -1228,21 +1228,135 @@ private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash)
12281228
}
12291229
}
12301230

1231-
var externalModuleDeps = parsedMetadataHash.ContainsKey("ExternalModuleDependencies") ?
1232-
parsedMetadataHash["ExternalModuleDependencies"] : null;
1231+
var externalModuleNames = GetExternalModuleDependencies(parsedMetadataHash);
1232+
foreach (var extModName in externalModuleNames)
1233+
{
1234+
if (dependenciesHash.ContainsKey(extModName))
1235+
{
1236+
dependenciesHash.Remove(extModName);
1237+
}
1238+
}
1239+
1240+
return dependenciesHash;
1241+
}
1242+
1243+
private static string[] GetExternalModuleDependencies(Hashtable parsedMetadataHash)
1244+
{
1245+
if (parsedMetadataHash == null)
1246+
{
1247+
return Array.Empty<string>();
1248+
}
1249+
1250+
var externalModules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
1251+
AddExternalModuleDependencies(parsedMetadataHash, externalModules);
1252+
1253+
if (TryGetHashtableValue(parsedMetadataHash, "PrivateData", out Hashtable privateData) &&
1254+
TryGetHashtableValue(privateData, "PSData", out Hashtable psData))
1255+
{
1256+
AddExternalModuleDependencies(psData, externalModules);
1257+
}
1258+
1259+
return externalModules.ToArray();
1260+
}
12331261

1234-
if (externalModuleDeps != null && LanguagePrimitives.TryConvertTo<string[]>(externalModuleDeps, out string[] externalModuleNames))
1262+
private static void AddExternalModuleDependencies(Hashtable container, HashSet<string> externalModules)
1263+
{
1264+
if (container == null || externalModules == null ||
1265+
!TryGetValue(container, "ExternalModuleDependencies", out object externalModuleDeps))
1266+
{
1267+
return;
1268+
}
1269+
1270+
if (externalModuleDeps == null)
1271+
{
1272+
return;
1273+
}
1274+
1275+
if (LanguagePrimitives.TryConvertTo<string[]>(externalModuleDeps, out string[] externalModuleNames) && externalModuleNames != null)
12351276
{
1236-
foreach (var extModName in externalModuleNames)
1277+
foreach (var name in externalModuleNames)
12371278
{
1238-
if (dependenciesHash.ContainsKey(extModName))
1279+
if (!string.IsNullOrWhiteSpace(name))
12391280
{
1240-
dependenciesHash.Remove(extModName);
1281+
externalModules.Add(name.Trim());
12411282
}
12421283
}
1284+
1285+
return;
12431286
}
12441287

1245-
return dependenciesHash;
1288+
if (LanguagePrimitives.TryConvertTo<string>(externalModuleDeps, out string externalModuleName) &&
1289+
!string.IsNullOrWhiteSpace(externalModuleName))
1290+
{
1291+
externalModules.Add(externalModuleName.Trim());
1292+
}
1293+
}
1294+
1295+
private static bool TryGetHashtableValue(Hashtable source, string key, out Hashtable value)
1296+
{
1297+
value = null;
1298+
if (!TryGetValue(source, key, out object rawValue) || rawValue == null)
1299+
{
1300+
return false;
1301+
}
1302+
1303+
if (rawValue is PSObject psObject && psObject.BaseObject != null)
1304+
{
1305+
rawValue = psObject.BaseObject;
1306+
}
1307+
1308+
if (rawValue is Hashtable hashtable)
1309+
{
1310+
value = hashtable;
1311+
return true;
1312+
}
1313+
1314+
if (rawValue is IDictionary dictionary)
1315+
{
1316+
var converted = new Hashtable(StringComparer.OrdinalIgnoreCase);
1317+
foreach (DictionaryEntry entry in dictionary)
1318+
{
1319+
converted[entry.Key] = entry.Value;
1320+
}
1321+
1322+
value = converted;
1323+
return true;
1324+
}
1325+
1326+
if (LanguagePrimitives.TryConvertTo<Hashtable>(rawValue, out Hashtable parsed) && parsed != null)
1327+
{
1328+
value = parsed;
1329+
return true;
1330+
}
1331+
1332+
return false;
1333+
}
1334+
1335+
private static bool TryGetValue(Hashtable source, string key, out object value)
1336+
{
1337+
value = null;
1338+
if (source == null || string.IsNullOrWhiteSpace(key))
1339+
{
1340+
return false;
1341+
}
1342+
1343+
if (source.ContainsKey(key))
1344+
{
1345+
value = source[key];
1346+
return true;
1347+
}
1348+
1349+
foreach (DictionaryEntry entry in source)
1350+
{
1351+
if (entry.Key is string entryKey &&
1352+
string.Equals(entryKey, key, StringComparison.OrdinalIgnoreCase))
1353+
{
1354+
value = entry.Value;
1355+
return true;
1356+
}
1357+
}
1358+
1359+
return false;
12461360
}
12471361

12481362
private bool CheckDependenciesExist(Hashtable dependencies, string repositoryName, NetworkCredential networkCredential)

test/PublishPSResourceTests/PublishPSResource.Tests.ps1

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,41 @@ Describe "Test Publish-PSResource" -tags 'CI' {
311311
{Publish-PSResource -Path $script:PublishModuleBase -ErrorAction Stop} | Should -Throw -ErrorId "DependencyNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource"
312312
}
313313

314+
It "Publish a module with ExternalModuleDependencies that are not published" {
315+
$version = "1.0.0"
316+
$externalModuleName = "PackageManagement"
317+
$manifestPath = Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1"
318+
$moduleFilePath = Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psm1"
319+
320+
@'
321+
function Test-PublishedFunction {
322+
Write-Output "OK"
323+
}
324+
'@ | Out-File -FilePath $moduleFilePath
325+
326+
@"
327+
@{
328+
RootModule = '$script:PublishModuleName.psm1'
329+
ModuleVersion = '$version'
330+
Author = 'None'
331+
Description = '$script:PublishModuleName module'
332+
GUID = '6f703037-3e95-4dcf-b06c-916f2867cce7'
333+
FunctionsToExport = @('Test-PublishedFunction')
334+
RequiredModules = @('$externalModuleName')
335+
PrivateData = @{
336+
PSData = @{
337+
ExternalModuleDependencies = @('$externalModuleName')
338+
}
339+
}
340+
}
341+
"@ | Out-File -FilePath $manifestPath
342+
343+
Publish-PSResource -Path $script:PublishModuleBase -Repository $testRepository2
344+
345+
$expectedPath = Join-Path -Path $script:repositoryPath2 -ChildPath "$script:PublishModuleName.$version.nupkg"
346+
(Get-ChildItem $script:repositoryPath2).FullName | Should -Be $expectedPath
347+
}
348+
314349
It "Publish a module with -SkipDependenciesCheck" {
315350
$version = "1.0.0"
316351
$dependencyVersion = "2.0.0"

0 commit comments

Comments
 (0)