PSSolutions.psm1
param() $PSModulesFolderName = "ps_modules" $PSScriptsFolderName = "ps_scripts" $PSSolutionFileName = "ps-solution.json" $PSSolutionLockFileName = "ps-solution-lock.json" . $PSScriptRoot/PsPolyfill.ps1 class PSSolution { [ValidateNotNullOrEmpty()][string]$Path } $ImportedPSSolutions = @{} function ThrowError { # Utility to throw an errorrecord param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $CallerPSCmdlet, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionName, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionMessage, [System.Object] $ExceptionObject, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, [parameter(Mandatory = $true)] [ValidateNotNull()] [System.Management.Automation.ErrorCategory] $ErrorCategory ) $exception = New-Object $ExceptionName $ExceptionMessage; $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $ErrorCategory, $ExceptionObject $CallerPSCmdlet.ThrowTerminatingError($errorRecord) } $OnRemoveScript = { $keys = $ImportedPSSolutions.Keys | % { $_ } foreach ($key in $keys) { Remove-PSSolution -Name $key } } $ExecutionContext.SessionState.Module.OnRemove += $OnRemoveScript function Get-PSSolutionModule { param( [Parameter(Position = 0, Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter(Mandatory = $false)] [switch] $SkipLoadData ) foreach ($dir in Get-ChildItem -LiteralPath $Path -Directory | ? ` {$_.Name -ne $PSModulesFolderName -and $_.Name -ne $PSScriptsFolderName}) { $moduleDataPath = Join-Path $dir.FullName "$($dir.Name).psd1" if (Test-Path $moduleDataPath -PathType Leaf) { if (!$SkipLoadData) { $moduleData = Import-PowerShellDataFile $moduleDataPath -ErrorAction Stop } [PSCustomObject]@{ "Name" = $dir.Name "LastWriteTime" = $dir.LastWriteTime "Data" = $moduleData } } } } function Get-PSSolution { param() $ImportedPSSolutions.keys | % {[PSCustomObject]@{ Name = $_ Path = $ImportedPSSolutions[$_].Path }} } function Get-LockData { param( [Parameter(Position = 0, Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Path ) $lockData = $null try { $lockDataRaw = Get-Content -Raw -Path (Join-Path $Path "$PSSolutionLockFileName") -ErrorAction Stop $lockData = $lockDataRaw | ConvertFrom-Json -ErrorAction Stop } catch { # Do nothing. } return $lockData } function Get-SolutionModulesState { param( $SolutionModules ) $SolutionModules ` | % { [PSCustomObject]@{ Name = $_.Name; State = [string]$_.LastWriteTime.Ticks }} ` | Sort-Object Name } function Save-ModuleFast { param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0, ParameterSetName = 'NameAndLiteralPathParameterSet')] [ValidateNotNullOrEmpty()] [string[]] $Name, [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'NameAndLiteralPathParameterSet')] [ValidateNotNull()] [string] $MinimumVersion, [Parameter(Mandatory = $true, ParameterSetName = 'NameAndLiteralPathParameterSet')] [string] $LiteralPath ) $module = Find-Module -Name $Name -MinimumVersion $MinimumVersion -ErrorAction Stop $targetModulePath = [IO.Path]::Combine($LiteralPath, $module.Name, $module.Version) if (!(Test-Path $targetModulePath -PathType Container)) { Save-Module -Name $Name -RequiredVersion $module.Version -LiteralPath $LiteralPath -AllowPrerelease -AcceptLicense -Force -ErrorAction Stop } } function Import-PSSolution { [outputtype("PSSolution")] param( [Parameter(Position = 0, Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $Name = "Default", [Parameter(Mandatory = $false)] [switch] $ForceReload = $false ) # Normalizing solution path $solutionPath = [IO.Path]::GetFullPath([IO.Path]::Combine((Get-Location).Path, $Path)) Write-Verbose "PSSolution directory: '$solutionPath'" # Checking solution path exists if (!(Test-Path $solutionPath -PathType Container)) { $errorMessage = "'$solutionPath' not found" ThrowError -ExceptionName "System.ArgumentException" ` -ExceptionMessage $errorMessage ` -ErrorId "PathNotFound" ` -CallerPSCmdlet $PSCmdlet ` -ExceptionObject $Path ` -ErrorCategory InvalidArgument } $oldSolution = $ImportedPSSolutions[$Name] $lockData = Get-LockData -Path $solutionPath # Performing checks for already imported solution if ($oldSolution) { Write-Verbose "Solution 'Name' already loaded, checking module states" # Disallowing changes of the solution path without explicit unload if ($oldSolution.Path -ne $solutionPath) { $errorMessage = "Solution with the name '$Name' already loaded with the different path '$($oldSolution.Path)'" ThrowError -ExceptionName "System.ArgumentException" ` -ExceptionMessage $errorMessage ` -ErrorId "DifferentPath" ` -CallerPSCmdlet $PSCmdlet ` -ExceptionObject $Path ` -ErrorCategory InvalidArgument } $solutionModules = Get-PSSolutionModule -Path $solutionPath -SkipLoadData $currentState = Get-SolutionModulesState @($solutionModules) $oldState = $lockData.moduleState # Comparing module states $stateAreEqual = $false if ($currentState.Count -eq $oldState.Count) { for ($i = 0; $i -lt $currentState.Count; $i++) { if ($currentState[$i].Name -ne $oldState[$i].Name ` -or $currentState[$i].State -ne $oldState[$i].State) { break; } } $stateAreEqual = $true } if ($stateAreEqual) { Write-Verbose "Modules state has not been changed" return; } } # Loading solution modules with their data $solutionModules = Get-PSSolutionModule -Path $solutionPath # Populating modules dictionary $solutionModulesDictionary = @{} $solutionModules | % {$solutionModulesDictionary.Add($_.Name, $_)} # Generating dependencies spec of the solution $depModules = @() foreach ($module in $solutionModules) { foreach ($moduleDep in $module.Data.RequiredModules) { if (!$solutionModulesDictionary.ContainsKey($moduleDep.ModuleName)) { $depModules += [PSCustomObject]@{ Name = $moduleDep.ModuleName MinimumVersion = $moduleDep.ModuleVersion } } } } # Normalizing dependencies specs $depModules = @($depModules | ` Group-Object {$_.Name + [char]0x0 + $_.MinimumVersion} | ` Sort-Object -Property Name | ` % {[PSCustomObject]@{ Name = $_.Group[0].Name MinimumVersion = $_.Group[0].MinimumVersion }}) $depModulesNotChanged = $false $oldDepModules = $lockData.DependencyModules if ($oldDepModules.Count -eq $depModules.Count) { for ($i = 0; $i -lt $oldDepModules.Count; $i++) { if ($oldDepModules[$i].Name -ne $depModules[$i].Name ` -or $oldDepModules[$i].MinimumVersion -ne $depModules[$i].MinimumVersion) { break; } } $depModulesNotChanged = $true } $depMdulesPath = Join-Path $solutionPath $PSModulesFolderName # TODO: Add ps_scripts update here # Updating dependency modules if ($depModulesNotChanged) { Write-Verbose "Dependency modules has not been changed." if ($oldSolution) { return; } } else { if ($oldSolution) { Write-Verbose "Dependency modules changed, unloading PSSolution" Remove-PSSolution -Name $Name -Verbose:$Verbose } $activityName = "Installing solution dependency modules" try { $totalDepsCount = $depModules.Count $i = 0 foreach ($dep in $depModules) { $percentComplete = [int](($i / $totalDepsCount) * 100) Write-Progress -Activity $activityName -Status "$percentComplete% Complete:" -PercentComplete $percentComplete; Save-ModuleFast -Name $dep.Name -MinimumVersion $dep.Version -LiteralPath $depMdulesPath $i++ } } finally { Write-Progress -Activity $activityName -Completed } } ConvertTo-Json @{ DependencyModules = @($depModules) ModuleState = @(Get-SolutionModulesState @($solutionModules)) } | Out-File -Encoding utf8 -FilePath (Join-Path $solutionPath $PSSolutionLockFileName) $solution = [PSSolution]@{ Path = $solutionPath } $ImportedPSSolutions.Add($Name, $solution) # Adding modules path to the PSModulesPath $psModulesParts = Get-PathVar -VarName PSModulePath $psModulesParts = @($solutionPath, $depMdulesPath) + $psModulesParts Set-PathVar -Parts $psModulesParts -VarName PSModulePath # TODO: Add ps_scripts and solution path to the $env:Path } function Remove-PSSolution { param( [Parameter(Position = 0)] [ValidateNotNullOrEmpty()] [string] $Name = "Default" ) if (!$ImportedPSSolutions.Contains($Name)) { $errorMessage = "Cannot find imported PSSolution with name '$Name'" ThrowError -ExceptionName "System.ArgumentException" ` -ExceptionMessage $errorMessage ` -ErrorId "PSSolutionNameAlreadyImported" ` -CallerPSCmdlet $PSCmdlet ` -ExceptionObject $Name ` -ErrorCategory InvalidArgument } $fullPath = $ImportedPSSolutions[$Name].Path; $depMdulesPath = Join-Path $fullPath $PSModulesFolderName # Removing solution module pathes from PSModulePathes $psModulesParts = Get-PathVar -VarName PSModulePath $psModulesParts = $psModulesParts | ? {($_ -ne $fullPath) -and ($_ -ne $depMdulesPath)} Set-PathVar -Parts $psModulesParts -VarName PSModulePath $ImportedPSSolutions.Remove($Name) $modulesToRemove = Get-Module | ? {($_.Path).StartsWith($fullPath)} $modulesToRemove | Remove-Module -Force } function Use-PSSolution() { } $exportModuleMemberParams = @{ Function = @( 'Get-PSSolution', 'Import-PSSolution', 'Remove-PSSolution' ) Variable = @( ) } Export-ModuleMember @exportModuleMemberParams |