@@ -71,14 +71,19 @@ function Install-ModuleFast {
7171 $WhatIfPreference = $currentWhatIfPreference
7272
7373 if ($plan.Count -eq 0 ) {
74+ if ($WhatIfPreference ) {
75+ Write-Host - fore DarkGreen ' ✅ No modules found to install or all modules are already installed.'
76+ }
7477 Write-Verbose ' No modules found to install or all modules are already installed. Exiting.'
7578 return
7679 }
7780
7881 if (-not $PSCmdlet.ShouldProcess ($Destination , " Install $ ( $plan.Count ) Modules" )) {
7982 Write-Host - fore DarkGreen ' 🚀 ModuleFast Install Plan BEGIN'
83+ # TODO: Separate planned installs and dependencies
8084 $plan
81- | Select-Object Name, @ {N = ' Version' ; E = { $_.Required } }
85+ | Select-Object Name, @ {N = ' Version' ; E = { [ModuleFastSpec ]::VersionToString($_.Required ) } }
86+ | Sort-Object Name
8287 | Format-Table - AutoSize
8388 | Out-String
8489 | Write-Host - ForegroundColor DarkGray
@@ -194,7 +199,7 @@ function Get-ModuleFastPlan {
194199 # This try finally is so that we can interrupt all http call tasks if Ctrl-C is pressed
195200 try {
196201 foreach ($moduleSpec in $ModulesToResolve ) {
197- $localMatch = Find-LocalModule $moduleSpec
202+ [ string ] $localMatch = Find-LocalModule $moduleSpec
198203 if ($localMatch -and -not $Update ) {
199204 Write-Verbose " Found local module $localMatch that satisfies $moduleSpec . Skipping..."
200205 # TODO: Capture this somewhere that we can use it to report in the deploy plan
@@ -254,6 +259,7 @@ function Get-ModuleFastPlan {
254259 # TODO: Type the responses and check on the type, not the existence of a property.
255260
256261 # HACK: Add the download URI to the catalog entry, this makes life easier.
262+ # TODO: This needs to be moved to a function so it isn't duplicated down in the "else" section below
257263 $pageLeaves = $response.items.items
258264 $pageLeaves | ForEach-Object {
259265 if ($PSItem.packageContent -and -not $PSItem.catalogEntry.packagecontent ) {
@@ -271,9 +277,12 @@ function Get-ModuleFastPlan {
271277 Limit-ModuleFastSpecVersions - ModuleSpec $currentModuleSpec - Highest - Versions $inlinedVersions
272278 }
273279
274- if ($versionMatch ) {
280+
281+ $selectedEntry = if ($versionMatch ) {
275282 Write-Debug " $currentModuleSpec `: Found satisfying version $versionMatch in the inlined index."
276- $selectedEntry = $entries | Where-Object version -EQ $versionMatch
283+
284+ # Output
285+ $entries | Where-Object version -EQ $versionMatch
277286 } else {
278287 # TODO: This should maybe be a separate function
279288
@@ -317,7 +326,7 @@ function Get-ModuleFastPlan {
317326 if (-not $pages ) {
318327 throw [InvalidOperationException ]" $currentModuleSpec `: a matching module was not found in the $Source repository that satisfies the version constraints. If this happens during dependency lookup, it is a bug in ModuleFast."
319328 }
320- Write-Debug " $currentModuleSpec `: Found $ ( $pages.Count ) additional pages that might match the query. "
329+ Write-Debug " $currentModuleSpec `: Found $ ( @ ( $pages ) .Count) additional pages that might match the query: $ ( $pages . ' @id ' -join ' , ' ) "
321330
322331 # TODO: This is relatively slow and blocking, but we would need complicated logic to process it in the main task handler loop.
323332 # I really should make a pipeline that breaks off tasks based on the type of the response.
@@ -330,7 +339,18 @@ function Get-ModuleFastPlan {
330339 while ($false -in $tasks.IsCompleted ) {
331340 [void ][Task ]::WaitAll($tasks , 500 )
332341 }
333- $entries = ($tasks.GetAwaiter ().GetResult() | ConvertFrom-Json ).items.catalogEntry
342+ $response = $tasks.GetAwaiter ().GetResult() | ConvertFrom-Json
343+ $items = $response.items
344+
345+ $pageLeaves = $items
346+ $pageLeaves | ForEach-Object {
347+ if ($PSItem.packageContent -and -not $PSItem.catalogEntry.packagecontent ) {
348+ $PSItem.catalogEntry
349+ | Add-Member - NotePropertyName ' PackageContent' - NotePropertyValue $PSItem.packageContent
350+ }
351+ }
352+
353+ $entries = $pageLeaves.catalogEntry
334354
335355 # TODO: Dedupe this logic with the above
336356 [HashSet [Version ]]$inlinedVersions = $entries.version
@@ -341,7 +361,9 @@ function Get-ModuleFastPlan {
341361 [Version ]$versionMatch = Limit-ModuleFastSpecVersions - ModuleSpec $moduleSpec - Versions $inlinedVersions - Highest
342362 if ($versionMatch ) {
343363 Write-Debug " $currentModuleSpec `: Found satisfying version $versionMatch in one of the additional pages."
344- $selectedEntry = $entries | Where-Object version -EQ $versionMatch
364+
365+ # Output
366+ $entries | Where-Object version -EQ $versionMatch
345367 # TODO: Resolve dependencies in separate function
346368 }
347369 }
@@ -350,6 +372,7 @@ function Get-ModuleFastPlan {
350372 throw ' Something other than exactly 1 selectedModule was specified. This should never happen and is a bug'
351373 }
352374
375+ if (-not $selectedEntry.packageContent ) { throw " No package content found for $ ( $selectedEntry.packageContent ) . This should never happen and is a bug" }
353376 [ModuleFastSpec ]$moduleInfo = [ModuleFastSpec ]::new(
354377 $selectedEntry.id ,
355378 $selectedEntry.version ,
@@ -430,16 +453,19 @@ function Get-ModuleFastPlan {
430453 # We do this here rather than populate modulesToResolve because the tasks wont start until all the existing tasks complete
431454 # TODO: Figure out a way to dedupe this logic maybe recursively but I guess a function would be fine too
432455 foreach ($dependencySpec in $dependenciesToResolve ) {
433- $localMatch = Find-LocalModule $dependencySpec
456+ [ string ] $localMatch = Find-LocalModule $dependencySpec
434457 if ($localMatch -and -not $Update ) {
435458 Write-Verbose " Found local module $localMatch that satisfies dependency $dependencySpec . Skipping..."
436459 # TODO: Capture this somewhere that we can use it to report in the deploy plan
437460 continue
461+ } else {
462+ Write-Debug " No local modules that satisfies dependency $dependencySpec . Checking Remote..."
438463 }
439464 # TODO: Deduplicate in-flight queries (az.accounts is a good example)
440465 # Write-Debug "$moduleSpec`: Checking if $dependencySpec already has an in-flight request that satisfies the requirement"
441466
442467 Write-Debug " $currentModuleSpec `: Fetching dependency $dependencySpec "
468+ # TODO: Do a direct version lookup if the dependency is a required version
443469 $task = Get-ModuleInfoAsync @httpContext - Endpoint $Source - Name $dependencySpec.Name
444470 $resolveTasks [$task ] = $dependencySpec
445471 # Used to track progress as tasks can get removed
@@ -793,18 +819,28 @@ class ModuleFastSpec : IComparable {
793819 [string ] ToString() {
794820 $name = $this._Name + ($this._Guid -ne [Guid ]::Empty ? " [$ ( $this._Guid ) ]" : ' ' )
795821 $versionString = switch ($true ) {
796- ($this.Min -eq [ModuleFastSpec ]::MinVersion -and $this.Max -eq [ModuleFastSpec ]::MaxVersion) {
822+ ($this.Min -eq [ModuleFastSpec ]::MinVersion -and $this.Max -eq [ModuleFastSpec ]::MaxVersion) {
797823 # This is the default, so we don't need to print it
798824 break
799825 }
800- ($null -ne $this.required ) { " @$ ( $this.Required ) " ; break }
801- ($this.Min -eq [ModuleFastSpec ]::MinVersion) { " <$ ( $this.Max ) " ; break }
802- ($this.Max -eq [ModuleFastSpec ]::MaxVersion) { " >$ ( $this.Min ) " ; break }
826+ ($null -ne $this.required ) { " @$ ( [ ModuleFastSpec ]::VersionToString( $this.Required ) ) " ; break }
827+ ($this.Min -eq [ModuleFastSpec ]::MinVersion) { " <$ ( [ ModuleFastSpec ]::VersionToString( $this.Max ) ) " ; break }
828+ ($this.Max -eq [ModuleFastSpec ]::MaxVersion) { " >$ ( [ ModuleFastSpec ]::VersionToString( $this.Min ) ) " ; break }
803829 default { " :$ ( $this.Min ) -$ ( $this.Max ) " }
804830 }
805831 return $name + $versionString
806832 }
807833
834+ # Converts a stored version to a string representation. This handles cases where the value was originally a System.Version
835+ static [string ] VersionToString([SemanticVersion ]$version ) {
836+ if ($null -eq $version ) { return $null }
837+ if ($Version.BuildLabel -match ' SYSTEMVERSION' -and $version.PrereleaseLabel -as [int ]) {
838+ # This is a system version, we need to convert it back to a system version
839+ return [ModuleFastSpec ]::ParseSemanticVersion($version ).ToString()
840+ }
841+ return $version.ToString ()
842+ }
843+
808844 # BUG: We cannot implement IEquatable directly because we need to self-reference ModuleFastSpec before it exists.
809845 # We can however just add Equals() method
810846
@@ -948,8 +984,8 @@ class NugetRange {
948984 return
949985 }
950986 $minString , $maxString = $range.split (' ,' )
951- if (-not [String ]::IsNullOrWhiteSpace($minString.trim ())) { $minString .trim ( ) }
952- if (-not [String ]::IsNullOrWhiteSpace($maxString.trim ())) { $maxString .trim ( ) }
987+ if (-not [String ]::IsNullOrWhiteSpace($minString )) { $this .Min = [ ModuleFastSpec ]::ParseVersionString( $minString ) }
988+ if (-not [String ]::IsNullOrWhiteSpace($maxString )) { $this .Max = [ ModuleFastSpec ]::ParseVersionString( $maxString ) }
953989 }
954990
955991 static [SemanticVersion ] Decrement([SemanticVersion ]$version ) {
@@ -1126,17 +1162,17 @@ function Find-LocalModule {
11261162 }
11271163
11281164 # Linux/Mac support requires a case insensitive search on a user supplied variable.
1129- $moduleDir = [Directory ]::GetDirectories($modulePath , $moduleSpec.Name , [EnumerationOptions ]@ {MatchCasing = ' CaseInsensitive' })
1130- if ($moduleDir .count -gt 1 ) { throw " $ ( $moduleSpec.Name ) folder is ambiguous, please delete one of these folders: $moduleDir " }
1131- if (-not $moduleDir ) {
1165+ $moduleBaseDir = [Directory ]::GetDirectories($modulePath , $moduleSpec.Name , [EnumerationOptions ]@ {MatchCasing = ' CaseInsensitive' })
1166+ if ($moduleBaseDir .count -gt 1 ) { throw " $ ( $moduleSpec.Name ) folder is ambiguous, please delete one of these folders: $moduleBaseDir " }
1167+ if (-not $moduleBaseDir ) {
11321168 Write-Debug " $modulePath does not have a $ ( $moduleSpec.Name ) folder. Skipping..."
11331169 continue
11341170 }
11351171
11361172 if ($moduleSpec.Required ) {
11371173 # We can speed up the search for explicit requiredVersion matches
11381174 $moduleVersion = $ModuleSpec.Version # We want to search using a nuget translated path
1139- $moduleFolder = Join-Path $modulePath $ModuleSpec .Name $moduleVersion
1175+ $moduleFolder = Join-Path $moduleBaseDir $moduleVersion
11401176
11411177 $manifestPath = Join-Path $moduleFolder " $ ( $ModuleSpec.Name ) .psd1"
11421178
@@ -1148,43 +1184,65 @@ function Find-LocalModule {
11481184 if ($manifestPath.count -eq 1 ) { return $manifestPath }
11491185 }
11501186 } else {
1151- $folders = [System.IO.Directory ]::GetDirectories($moduleDir ) | Split-Path - Leaf
1152- [Version []]$candidateVersions = foreach ($folder in $folders ) {
1187+ # This is used to keep a map of versions to manifests, needed to support both "classic" and "versioned" module folders
1188+ [Dictionary [Version , String ]]$candidateVersions = @ {}
1189+
1190+ # If not a classic module, check for versioned folders
1191+ $folders = [Directory ]::GetDirectories($moduleBaseDir )
1192+ foreach ($folder in $folders ) {
1193+ $versionCandidate = Split-Path - Leaf $folder
11531194 [Version ]$version = $null
1154- if ([Version ]::TryParse($folder , [ref ]$version )) {
1155- $version
1195+ if ([Version ]::TryParse($versionCandidate , [ref ]$version )) {
1196+ # Try to retrieve the manifest
1197+ # TODO: Create a "Assert-CaseSensitiveFileExists" function for this pattern used multiple times
1198+ $versionedManifestPath = [Directory ]::GetFiles($folder , " $ ( $ModuleSpec.Name ) .psd1" , [EnumerationOptions ]@ {MatchCasing = ' CaseInsensitive' })
1199+ if ($versionedManifestPath.count -gt 1 ) { throw " $folder manifest is ambiguous, please delete one of these: $versionedManifestPath " }
1200+ if ($versionedManifestPath ) {
1201+ $candidateVersions.Add ($version , $versionedManifestPath )
1202+ }
11561203 } else {
1157- # Check for a "classic" non-versioned module folder and get the version from the manifest
1158- $manifestPath = [ Directory ]::GetFiles(( Join-Path $modulePath $moduleSpec .Name ) , " $ ( $ModuleSpec .Name ) .psd1 " , [ EnumerationOptions ] @ { MatchCasing = ' CaseInsensitive ' })
1159- if ( $manifestPath .count -gt 1 ) { throw " $moduleFolder manifest is ambiguous, please delete one of these: $manifestPath " }
1204+ Write-Debug " Could not parse $folder in $moduleBaseDir as a valid version. This is either a bad version directory or this folder is a classic module. "
1205+ continue
1206+ }
11601207
1161- if ($manifestPath ) {
1162- $manifestData = Import-PowerShellDataFile $manifestPath
1208+ # Check for a "classic" module if no versioned folders were found
1209+ if ($candidateVersions.count -eq 0 ) {
1210+ $classicManifestPath = [Directory ]::GetFiles($moduleBaseDir , " $ ( $ModuleSpec.Name ) .psd1" , [EnumerationOptions ]@ {MatchCasing = ' CaseInsensitive' })
1211+ if ($classicManifestPath.count -gt 1 ) { throw " $moduleBaseDir manifest is ambiguous, please delete one of these: $classicManifestPath " }
1212+ if ($classicManifestPath ) {
1213+ $manifestData = Import-PowerShellDataFile $classicManifestPath
11631214 # Return the version in the manifest
1164- $manifestData.ModuleVersion
1215+ $candidateVersions.Add ($manifestData.ModuleVersion , $classicManifestPath )
1216+ continue
11651217 }
1218+ }
11661219
1167- Write-Warning " Could not parse $folder in $moduleDir as a valid version. This is probably a bad module directory and should be removed."
1220+ if (-not $candidateVersions.count ) {
1221+ Write-Verbose " $moduleSpec `: module folder exists at $moduleBaseDir but no modules found that match the version spec."
1222+ continue
11681223 }
1169- }
11701224
1171- if (-not $candidateVersions ) {
1172- Write-Verbose " $moduleSpec `: module folder exists at $moduleDir but no modules found that match the version spec."
1173- continue
1174- }
1175- $versionMatch = Limit-ModuleFastSpecVersions - ModuleSpec $ModuleSpec - Versions $candidateVersions - Highest
1176- if ($versionMatch ) {
1177- $manifestPath = Join-Path $moduleDir $ ([Version ]$versionMatch ) " $ ( $ModuleSpec.Name ) .psd1"
1178- if (-not [File ]::Exists($manifestPath )) {
1179- # Our matching method doesn't make it easy to match on "next highest" version, so we have to do this.
1180- throw " A matching module folder was found for $ModuleSpec but the manifest is not present at $manifestPath . This indicates a corrupt module and should be removed before proceeding."
1181- } else {
1182- return $manifestPath
1225+ [Version ]$versionMatch = Limit-ModuleFastSpecVersions - ModuleSpec $ModuleSpec - Versions $candidateVersions.Keys - Highest
1226+ if (-not $versionMatch ) {
1227+ [string []]$candidateStrings = foreach ($candidate in $candidateVersions.keys ) {
1228+ ' {0} ({1})' -f $candidateVersions [$candidate ], $candidate
1229+ }
1230+ Write-Debug " $moduleSpec `: Module versions were found but none that match the module spec requirement. Found Candidates: $ ( $candidateStrings -join ' ;' ) "
1231+ return $null
1232+ }
1233+
1234+ [string ]$matchingManifest = $candidateVersions [$versionMatch ]
1235+
1236+ if (-not [File ]::Exists($matchingManifest )) {
1237+ throw " A matching module folder was found for $ModuleSpec but the manifest is not present at $matchingManifest . This is a bug and should never happen as we should have checked this ahead of time."
11831238 }
1239+ # TODO: Verify the manifest isn't corrupt by checking the module version? Should we be doing this for all manifests even tho it's a perf hit? Configurable option makes sense
1240+
1241+ return $matchingManifest
11841242 }
11851243 }
11861244 }
1187- return $false
1245+ return $null
11881246}
11891247
11901248# Find all normalized versions of a version, for example 1.0.1.0 also is 1.0.1
0 commit comments