diff --git a/.gitignore b/.gitignore index 2442a77..e765cea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,12 @@ ## Those files should be taken from their repositary -vendor/* -!vendor/*.md -!vendor/*.bat -!vendor/*.json +vendor/*/* +!vendor/* +!vendor/psmodules/PsGet config/.history Thumbs.db *.exe build/ -Version v* \ No newline at end of file +Version v* diff --git a/config/ConEmu.xml b/config/ConEmu.xml index 19fd4c5..da6300a 100644 --- a/config/ConEmu.xml +++ b/config/ConEmu.xml @@ -419,8 +419,8 @@ - - + + @@ -487,7 +487,7 @@ - + @@ -495,13 +495,21 @@ - - + + - + + + + + + + + + diff --git a/vendor/profile.ps1 b/vendor/profile.ps1 new file mode 100644 index 0000000..0c5a7c3 --- /dev/null +++ b/vendor/profile.ps1 @@ -0,0 +1,39 @@ +# Global modules directory +$global:PsGetDestinationModulePath = $PSScriptRoot + "\..\vendor\psmodules" + +# Push to modules location +Push-Location -Path ($PsGetDestinationModulePath) + +# Load modules from current directory +Get-ChildItem -Directory | ` +Foreach-Object{ + Import-Module .\$_\$_ +} + +# Come back to PWD +Pop-Location + +# Set up a Cmder prompt, adding the git prompt parts inside git repos +function global:prompt { + $realLASTEXITCODE = $LASTEXITCODE + $Host.UI.RawUI.ForegroundColor = "white" + Write-Host($pwd.ProviderPath) -NoNewLine -ForegroundColor "green" + if (Get-Module posh-git) { + Write-VcsStatus + } + $global:LASTEXITCODE = $realLASTEXITCODE + return "`nλ " +} + +# Load special features come from posh-git +if (Get-Module posh-git) { + Enable-GitColors + Start-SshAgent -Quiet +} + +# Move to the wanted location +if (Test-Path Env:\CMDER_START) { + Set-Location -Path $Env:CMDER_START +} elseif ($Env:CMDER_ROOT -and $Env:CMDER_ROOT.StartsWith($pwd)) { + Set-Location -Path $Env:USERPROFILE +} diff --git a/vendor/psmodules/PsGet/PsGet.psd1 b/vendor/psmodules/PsGet/PsGet.psd1 new file mode 100644 index 0000000..5534b73 --- /dev/null +++ b/vendor/psmodules/PsGet/PsGet.psd1 @@ -0,0 +1,88 @@ +@{ +# These modules will be processed when the module manifest is loaded. This is only supported in PS 3.0 +#RootModule = "PsGet.psm1" + +#This is required for PS 2.0 +ModuleToProcess = "PsGet.psm1" + +# The version of this module. +ModuleVersion = '1.0' + +# This GUID is used to uniquely identify this module. +GUID = '638FF397-8108-4B94-981A-D9BDAB4774B2' + +# The author of this module. +Author = 'Mike Chaliy' + +# The company or vendor for this module. +CompanyName = '' + +# The copyright statement for this module. +Copyright = '(c) 2013' + +# Description of the functionality provided by this module +Description = 'PsGet module for installing PowerShell modules' + +# Minimum version of the Windows PowerShell engine required by this module +PowerShellVersion = '2.0' + +# Name of the Windows PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the Windows PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of the .NET Framework required by this module +DotNetFrameworkVersion = '2.0' + +# Minimum version of the common language runtime (CLR) required by this module +CLRVersion = '2.0' + +# Processor architecture (None, X86, Amd64) required by this module +ProcessorArchitecture = 'None' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +NestedModules = @() + +# Functions to export from this module +FunctionsToExport = '*' + +# Cmdlets to export from this module +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module +AliasesToExport = '*' + +# List of all modules packaged with this module. +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess +# PrivateData = '' + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' +} \ No newline at end of file diff --git a/vendor/psmodules/PsGet/PsGet.psm1 b/vendor/psmodules/PsGet/PsGet.psm1 new file mode 100644 index 0000000..1a2412f --- /dev/null +++ b/vendor/psmodules/PsGet/PsGet.psm1 @@ -0,0 +1,2132 @@ +<# +.SYNOPSIS + PowerShell module installation stuff. + URL: https://github.com/psget/psget + Based on http://poshcode.org/1875 Install-Module by Joel Bennett +#> +#requires -Version 2.0 + +#region Setup + +Write-Debug 'Set up the global scope config variables.' +$global:UserModuleBasePath = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath 'WindowsPowerShell\Modules' +$global:CommonGlobalModuleBasePath = Join-Path -Path $env:CommonProgramFiles -ChildPath 'Modules' + +if (-not (Test-Path -Path:variable:global:PsGetDirectoryUrl)) { + $global:PsGetDirectoryUrl = 'https://github.com/psget/psget/raw/master/Directory.xml' +} +# NOTE: $global:PsGetDestinationModulePath is used by Install-Module as configuration if set by user. + +Write-Debug 'Set up needed constants.' +Set-Variable -Name PSGET_ZIP -Value 'ZIP' -Option Constant -Scope Script +Set-Variable -Name PSGET_PSM1 -Value 'PSM1' -Option Constant -Scope Script +Set-Variable -Name PSGET_PSD1 -Value 'PSD1' -Option Constant -Scope Script + +#endregion + +#region Exported Cmdlets + +<# + .SYNOPSIS + Installs PowerShell modules from a variety of sources including: Nuget, PsGet module directory, local directory, zipped folder and web URL. + + .DESCRIPTION + Supports installing modules for the current user or all users (if elevated). + + .PARAMETER Module + Name of the module to install. + + .PARAMETER ModuleUrl + URL to the module to install; Can be direct link to PSM1 file or ZIP file. Can be a shortened link. + + .PARAMETER ModulePath + Local path to the module to install. + + .PARAMETER ModuleName + In context with -ModuleUrl or -ModulePath it is not always possible to interfere the right ModuleName, eg. the filename is unknown or the zip archive contains multiple modules. + + .PARAMETER Type + When ModuleUrl or ModulePath specified, allows specifying type of the package. Can be ZIP or PSM1. + + .PARAMETER NuGetPackageId + NuGet package name containing the module to install. + + .PARAMETER PackageVersion + Allows a specific version of the specified NuGet package to used, if not specified then the latest stable version will be used. + + .PARAMETER NugetSource + URL to the NuGet feed containing the package. + + .PARAMETER PreRelease + If PackageVersion is not specified, then this switch allows the latest prerelease package to be used. + + .PARAMETER PreReleaseTag + If PackageVersion is not specified, then this parameter allows the latest version of a particular prerelease tag to be used + + .PARAMETER Destination + When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined. + + .PARAMETER ModuleHash + When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash. + + .PARAMETER Global + If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules... + + NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined. + + .PARAMETER DoNotImport + Indicates that command should not import module after installation + + .PARAMETER AddToProfile + Adds Import-Module statement for installed module to the profile.ps1 + + .PARAMETER Update + Forces module to be updated + + .PARAMETER DirectoryUrl + URL to central directory. By default it uses the value in the $global:PsGetDirectoryUrl variable + + .PARAMETER PersistEnvironment + If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified) + + .PARAMETER InstallWithModuleName + Allows to specify the name of the module and override the ModuleName normally used. + NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name. + + .PARAMETER DoNotPostInstall + If defined, the PostInstallHook is not executed. + + .PARAMERTER PostInstallHook + Defines the name of a script inside the installed module folder which should be executed after installation. + Default: definition in directory file or 'Install.ps1' + + .PARAMETER Force + OBSOLATE + Alternative name for 'Update'. + + .PARAMETER Startup + OBSOLATE + Alternative name for 'AddToProfile'. + + .LINK + http://psget.net + + .EXAMPLE + # Install-Module PsConfig -DoNotImport + + Description + ----------- + Installs the module witout importing it to the current session + + .EXAMPLE + # Install-Module PoshHg -AddToProfile + + Description + ----------- + Installs the module and then adds impoer of the given module to your profile.ps1 file + + .EXAMPLE + # Install-Module PsUrl + + Description + ----------- + This command will query module information from central registry and install required stuff. + + .EXAMPLE + # Install-Module -ModulePath .\Authenticode.psm1 -Global + + Description + ----------- + Installs the Authenticode module to the System32\WindowsPowerShell\v1.0\Modules for all users to use. + + .EXAMPLE + # Install-Module -ModuleUrl https://github.com/chaliy/psurl/raw/master/PsUrl/PsUrl.psm1 + + Description + ----------- + Installs the PsUrl module to the users modules folder + + .EXAMPLE + # Install-Module -ModuleUrl http://bit.ly/e1X4BO -ModuleName "PsUrl" + + Description + ----------- + Installs the PsUrl module with name specified, because command will not be able to guess it + + .EXAMPLE + # Install-Module -ModuleUrl https://github.com/psget/psget/raw/master/TestModules/HelloWorld.zip + + Description + ----------- + Downloads HelloWorld module (module can have more than one file) and installs it + + .EXAMPLE + # Install-Module -NugetPackageId SomePackage + + Description + ----------- + Downloads the latest stable version of the 'SomePackage' module from the NuGet Gallery + + .EXAMPLE + # Install-Module -NugetPackageId SomePackage -PackageVersion 1.0.2-beta + + Description + ----------- + Downloads the specified version of the 'SomePackage' module from the NuGet Gallery + + .EXAMPLE + # Install-Module -NugetPackageId SomePackage -PreRelease + + Description + ----------- + Downloads the latest pre-release version of the 'SomePackage' module from the NuGet Gallery + + .EXAMPLE + # Install-Module -NugetPackageId SomePackage -PreReleaseTag beta -NugetSource http://myget.org/F/myfeed + + Description + ----------- + Downloads the latest 'beta' pre-release version of the 'SomePackage' module from a custom NuGet feed +#> +function Install-Module { + [CmdletBinding()] + param ( + [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true, ParameterSetName='CentralDirectory')] + [String] $Module, + + [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true, ParameterSetName='Web')] + [String] $ModuleUrl, + + [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true, ParameterSetName='Local')] + [String] $ModulePath, + + [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='Web')] + [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='Local')] + [String] $ModuleName, + + [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='Web')] + [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='Local')] + [ValidateSet('ZIP', 'PSM1', 'PSD1', '')] # $script:PSGET_ZIP, $script:PSGET_PSM1 or $script:PSGET_PSD1 + [String] $Type, + + [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true, ParameterSetName='NuGet')] + [ValidatePattern('^\w+([_.-]\w+)*$')] # regex from NuGet.PackageIdValidator._idRegex + [ValidateLength(1,100)] # maximum length from NuGet.PackageIdValidator.MaxPackageIdLength + [String] $NuGetPackageId, + + [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='NuGet')] + [String] $PackageVersion, + + [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='NuGet')] + [String] $NugetSource = 'https://nuget.org/api/v2/', + + [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='NuGet')] + [Switch] $PreRelease, + + [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='NuGet')] + [String] $PreReleaseTag, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $Destination = $global:PsGetDestinationModulePath, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $ModuleHash, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Global, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotImport, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $AddToProfile, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Update, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $DirectoryUrl = $global:PsGetDirectoryUrl, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $PersistEnvironment, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $InstallWithModuleName, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotPostInstall, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $PostInstallHook, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Force, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Startup + ) + process { + + if ($Force) { + Write-Verbose 'Force parameter is considered obsolete. Please use Update instead.' + $Update = $true + } + + if ($Startup) { + Write-Verbose 'Startup parameter is considered obsolete. Please use AddToProfile instead.' + $AddToProfile = $true + } + + if (-not $Destination) { + $Destination = if ($Global) { $global:CommonGlobalModuleBasePath } else { $global:UserModuleBasePath } + + #Because we are using the default location, always ensure it is persisted + $PersistEnvironment = $true + } + + if (-not $Destination) { + throw 'The destination path was not added to the PSModulePath environment variable, ensure you have the rights to modify environment variables' + } + + $Destination = ConvertTo-CanonicalPath -Path $Destination + + Write-Debug "Execute installation for '$($PSCmdlet.ParameterSetName)' type." + + switch($PSCmdlet.ParameterSetName) { + CentralDirectory { + Install-ModuleFromDirectory -Module:$Module -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DirectoryUrl:$DirectoryUrl -InstallWithModuleName:$InstallWithModuleName -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook + } + Web { + Install-ModuleFromWeb -ModuleUrl:$ModuleUrl -ModuleName:$ModuleName -Type:$Type -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -InstallWithModuleName:$InstallWithModuleName -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook + } + Local { + Install-ModuleFromLocal -ModulePath:$ModulePath -ModuleName:$ModuleName -Type:$Type -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -InstallWithModuleName:$InstallWithModuleName -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook + } + NuGet { + Install-ModuleFromNuGet -NuGetPackageId:$NuGetPackageId -PackageVersion:$PackageVersion -NugetSource:$NugetSource -PreRelease:$PreRelease -PreReleaseTag:$PreReleaseTag -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -InstallWithModuleName:$InstallWithModuleName -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook + } + default { + throw "Unknown ParameterSetName '$($PSCmdlet.ParameterSetName)'" + } + } + } +} + +<# + .SYNOPSIS + Updates a module. + + .DESCRIPTION + Supports updating modules for the current user or all users (if elevated). + + .PARAMETER Module + Name of the module to update. + + .PARAMETER All + If -All is defined. all to PsGet known modules will be updated. + + .PARAMETER Destination + When specified the module will be updated below this path. + + .PARAMETER ModuleHash + When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash. + + .PARAMETER Global + If set, attempts to install the module to the all users location in Windows\System32... + + .PARAMETER DoNotImport + Indicates that command should not import module after installation. + + .PARAMETER AddToProfile + Adds installed module to the profile.ps1. + + .PARAMETER Update + Forces module to be updated. + + .PARAMETER DirectoryUrl + URL to central directory. By default it uses the value in the $PsGetDirectoryUrl global variable. + + .PARAMETER DoNotPostInstall + If defined, the PostInstallHook is not executed. + + .PARAMERTER PostInstallHook + Defines the name of a script inside the installed module folder which should be executed after installation. + Will not be check in combination with -All switch. + Default: 'Install.ps1' + + .LINK + http://psget.net + + .LINK + Install-Module + + .EXAMPLE + # Update-Module PsUrl + + Description + ----------- + Updates the module +#> +function Update-Module { + [CmdletBinding()] + param ( + [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)] + [String] $Module, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $All, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $Destination = $global:PsGetDestinationModulePath, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $ModuleHash, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Global, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotImport, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $AddToProfile, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $DirectoryUrl = $global:PsGetDirectoryUrl, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotPostInstall, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $PostInstallHook + ) + process { + if ($All) { + Install-Module -Module PSGet -Force -DoNotImport + + Get-PsGetModuleInfo -ModuleName '*' | Where-Object { + if ($_.Id -ne 'PSGet') { + Get-Module -Name:($_.ModuleName) -ListAvailable + } + } | Install-Module -Update + + Import-Module -Name PSGet -Force -DoNotPostInstall:$DoNotPostInstall + + } + else { + Install-Module -Module:$Module -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -DirectoryUrl:$DirectoryUrl -Updat -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook + } + } +} + +<# + .SYNOPSIS + Retrieve information about module from central directory + + .DESCRIPTION + Command will query central directory to get information about module specified. + + .PARAMETER ModuleName + Name of module to look for in directory. Supports wildcards. + + .PARAMETER DirectoryUrl + URL to central directory. By default it uses the value in the $PsGetDirectoryUrl global variable. + + .LINK + http://psget.net + + .EXAMPLE + Get-PsGetModuleInfo PoshCo* + + Description + ----------- + Retrieves information about all registerd modules that starts with PoshCo. +#> +function Get-PsGetModuleInfo { + [CmdletBinding()] + param ( + [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)] + [String] $ModuleName, + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $DirectoryUrl = $global:PsGetDirectoryUrl + ) + begin { + $client = (new-object Net.WebClient) + $client.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + + $PsGetDataPath = Join-Path -Path $Env:APPDATA -ChildPath psget + $DirectoryCachePath = Join-Path -Path $PsGetDataPath -ChildPath directorycache.clixml + $DirectoryCache = @() + $CacheEntry = $null + if (Test-Path -Path $DirectoryCachePath) { + $DirectoryCache = Import-Clixml -Path $DirectoryCachePath + $CacheEntry = $DirectoryCache | Where-Object { $_.Url -eq $DirectoryUrl } | Select-Object -First 1 + } + if (-not $CacheEntry) { + $CacheEntry = @{ + Url = $DirectoryUrl + File = '{0}.xml' -f [Guid]::NewGuid().Tostring() + ETag = $null + } + $DirectoryCache += @($CacheEntry) + } + $CacheEntryFilePath = Join-Path -Path $PsGetDataPath -ChildPath $CacheEntry.File + if ($CacheEntry -and $CacheEntry.ETag -and (Test-Path -Path $CacheEntryFilePath)) { + if ((Get-Item -Path $CacheEntryFilePath).LastWriteTime.AddDays(1) -gt (Get-Date)) { + # use cached directory if it is less than 24 hours old + $client.Headers.Add('If-None-Match', $CacheEntry.ETag) + } + } + + try { + Write-Verbose "Downloading modules repository from $DirectoryUrl" + $repoRaw = $client.DownloadString($DirectoryUrl) + $StatusCode = 200 + } + catch [System.Net.WebException] { + $Response = $_.Exception.Response + if ($Response) { $StatusCode = [int]$Response.StatusCode } + } + + if ($StatusCode -eq 200) { + $repoXml = [xml]$repoRaw + + $CacheEntry.ETag = $client.ResponseHeaders['ETag'] + if (-not (Test-Path -Path $PsGetDataPath)) { + New-Item -Path $PsGetDataPath -ItemType Container | Out-Null + } + $repoXml.Save($CacheEntryFilePath) + Export-Clixml -InputObject $DirectoryCache -Path $DirectoryCachePath + } + elseif (Test-Path -Path $CacheEntryFilePath) { + if ($StatusCode -ne 304) { + Write-Warning "Could not retrieve modules repository from '$DirectoryUrl'. Status code: $StatusCode" + } + Write-Verbose 'Using cached copy of modules repository' + $repoXml = [xml](Get-Content -Path $CacheEntryFilePath) + } + else { + throw "Could not retrieve modules repository from '$DirectoryUrl'. Status code: $StatusCode" + } + + $nss = @{ a = 'http://www.w3.org/2005/Atom'; + pg = 'urn:psget:v1.0' } + + $feed = $repoXml.feed + $title = $feed.title.innertext + Write-Verbose "Processing $title feed..." + } + process { + # Very naive, ignoring namespaces and so on. + $feed.entry | Where-Object { $_.id -like $ModuleName } | + ForEach-Object { + $Type = '' + switch -regex ($_.content.type) { + 'application/zip' { $Type = $PSGET_ZIP } + default { $Type = $PSGET_PSM1 } + } + + $Verb = if ($_.properties.Verb -imatch 'POST') { 'POST' } else { 'GET' } + + New-Object PSObject -Property @{ + Title = $_.title.innertext + Description = $_.summary.'#text' + Updated = [DateTime]$_.updated + Author= $_.author.name + Id = $_.id + ModuleName = if ($_.properties.ModuleName) { $_.properties.ModuleName } else { $_.id } + Type = $Type + DownloadUrl = $_.content.src + Verb = $Verb + #This was changed from using the $_.properties.ProjectUrl because the value for ModuleUrl needs to be the full path to the module file + #This change was required to get the tests to pass + ModuleUrl = $_.content.src + NoPostInstallHook = if ($_.properties.NoPostInstallHook -eq 'true') { $true } else { $false } + PostInstallHook = $_.properties.PostInstallHook + PostUpdateHook = $_.properties.PostUpdateHook + } | + Select-Object Title, ModuleName, Id, Description, Updated, Type, Verb, ModuleUrl, DownloadUrl, NoPostInstallHook, PostInstallHook, PostUpdateHook + } + } +} + +<# + .SYNOPSIS + Calculate the hash value of a module. + + .DESCRIPTION + Calculate the hash value of the specified module directory for usage with the 'ModuleHash' parameter for validation. + + .PARAMETER Path + Path to the module directory + + .EXAMPLE + Get-PsGetModuleHash $global:UserModuleBasePath\PsGet + + Description + ----------- + Returns the hash value usable with the 'ModuleHash' parameter of 'Install-Module' + + .LINK + Install-Module + + .LINK + http://psget.net +#> +function Get-PsGetModuleHash { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [Alias('ModuleBase')] + [String] $Path + ) + process { + Get-FolderHash -Path (Resolve-Path -Path $Path).Path + } +} + +#endregion + +#region Sub-Cmdlets + +<# + .SYNOPSIS + Install a module from the defined PsGet directory. + + .PARAMETER Module + Name of the module to install from PsGet directory. + + .PARAMETER Destination + When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined. + + .PARAMETER ModuleHash + When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash. + + .PARAMETER Global + If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules... + + NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined. + + .PARAMETER DoNotImport + Indicates that command should not import module after installation + + .PARAMETER AddToProfile + Adds Import-Module statement for installed module to the profile.ps1 + + .PARAMETER Update + Forces module to be updated + + .PARAMETER DirectoryUrl + URL to central directory. By default it uses the value in the $global:PsGetDirectoryUrl variable + + .PARAMETER PersistEnvironment + If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified) + + .PARAMETER InstallWithModuleName + Allows to specify the name of the module and override the ModuleName normally used. + NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name. + + .PARAMETER DoNotPostInstall + If defined, the PostInstallHook is not executed. + + .PARAMERTER PostInstallHook + Defines the name of a script inside the installed module folder which should be executed after installation. + Default: definition in directory file or 'Install.ps1' +#> +function Install-ModuleFromDirectory { + [CmdletBinding()] + param ( + [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)] + [String] $Module, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $Destination = $global:PsGetDestinationModulePath, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $ModuleHash, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Global, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotImport, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $AddToProfile, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Update, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $DirectoryUrl = $global:PsGetDirectoryUrl, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $PersistEnvironment, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $InstallWithModuleName, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotPostInstall, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $PostInstallHook + ) + process { + $testModuleName = if ($InstallWithModuleName) { $InstallWithModuleName } else { $Module } + if (Test-ModuleInstalledAndImport -ModuleName:$testModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) { + return + } + + Write-Verbose "Module $Module will be installed from central repository" + $moduleData = Get-PsGetModuleInfo -ModuleName:$Module -DirectoryUrl:$DirectoryUrl | select -First 1 + if (-not $moduleData) { + throw "Module $Module was not found in central repository" + } + + # $Module and $moduleData.Id are not equally by garantee, so we have to test again. + if (Test-ModuleInstalledAndImport -ModuleName:$moduleData.ModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) { + return + } + + if (-not $DoNotPostInstall) { + $DoNotPostInstall = $moduledata.NoPostInstallHook + } + + if (-not $PostInstallHook) { + if ($Update) { + $PostInstallHook = $moduleData.PostUpdateHook + } + else { + $PostInstallHook = $moduleData.PostInstallHook + } + + if (-not $PostInstallHook) { + $PostInstallHook = 'Install.ps1' + } + } + + $result = Invoke-DownloadModuleFromWeb -DownloadUrl:$moduleData.DownloadUrl -ModuleName:$moduleData.ModuleName -Type:$moduleData.Type -Verb:$moduleData.Verb + Install-ModuleToDestination -ModuleName:$result.ModuleName -InstallWithModuleName:$InstallWithModuleName -ModuleFolderPath:$result.ModuleFolderPath -TempFolderPath:$result.TempFolderPath -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook + } +} + +<# + .SYNOPSIS + Install a module from a provided download location. + + .PARAMETER ModuleUrl + URL to the module to install; Can be direct link to PSM1 file or ZIP file. Can be a shortened link. + + .PARAMETER ModuleName + It is not always possible to interfere the right ModuleName, eg. the filename is unknown or the zip archive contains multiple modules. + + .PARAMETER Type + When ModuleUrl or ModulePath specified, allows specifying type of the package. Can be ZIP or PSM1. + + .PARAMETER Destination + When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined. + + .PARAMETER ModuleHash + When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash. + + .PARAMETER Global + If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules... + + NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined. + + .PARAMETER DoNotImport + Indicates that command should not import module after installation + + .PARAMETER AddToProfile + Adds Import-Module statement for installed module to the profile.ps1 + + .PARAMETER Update + Forces module to be updated + + .PARAMETER PersistEnvironment + If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified) + + .PARAMETER InstallWithModuleName + Allows to specify the name of the module and override the ModuleName normally used. + NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name. + + .PARAMETER DoNotPostInstall + If defined, the PostInstallHook is not executed. + + .PARAMERTER PostInstallHook + Defines the name of a script inside the installed module folder which should be executed after installation. + Default: 'Install.ps1' +#> +function Install-ModuleFromWeb { + [CmdletBinding()] + param ( + [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)] + [String] $ModuleUrl, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $ModuleName, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [ValidateSet('ZIP', 'PSM1', 'PSD1', '')] # $script:PSGET_ZIP, $script:PSGET_PSM1 or $script:PSGET_PSD1 + [String] $Type, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $Destination = $global:PsGetDestinationModulePath, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $ModuleHash, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Global, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotImport, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $AddToProfile, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Update, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $PersistEnvironment, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $InstallWithModuleName, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotPostInstall, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $PostInstallHook + ) + process { + Write-Verbose "Module will be installed from $ModuleUrl" + + if ($InstallWithModuleName) { + if (Test-ModuleInstalledAndImport -ModuleName:$InstallWithModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) { + return + } + } + + $result = Invoke-DownloadModuleFromWeb -DownloadUrl:$ModuleUrl -ModuleName:$ModuleName -Type:$Type -Verb:'GET' + + if (-not $PostInstallHook) { + $PostInstallHook = 'Install.ps1' + } + + Install-ModuleToDestination -ModuleName:$result.ModuleName -InstallWithModuleName:$InstallWithModuleName -ModuleFolderPath:$result.ModuleFolderPath -TempFolderPath:$result.TempFolderPath -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook + } +} + +<# + .SYNOPSIS + Install a module from a provided local path. + + .PARAMETER ModulePath + Local path to the module to install. + + .PARAMETER ModuleName + It is not always possible to interfere the right ModuleName, eg. the filename is unknown or the zip archive contains multiple modules. + + .PARAMETER Type + When ModuleUrl or ModulePath specified, allows specifying type of the package. Can be ZIP or PSM1. + + .PARAMETER Destination + When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined. + + .PARAMETER ModuleHash + When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash. + + .PARAMETER Global + If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules... + + NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined. + + .PARAMETER DoNotImport + Indicates that command should not import module after installation + + .PARAMETER AddToProfile + Adds Import-Module statement for installed module to the profile.ps1 + + .PARAMETER Update + Forces module to be updated + + .PARAMETER PersistEnvironment + If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified) + + .PARAMETER InstallWithModuleName + Allows to specify the name of the module and override the ModuleName normally used. + NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name. + + .PARAMETER DoNotPostInstall + If defined, the PostInstallHook is not executed. + + .PARAMERTER PostInstallHook + Defines the name of a script inside the installed module folder which should be executed after installation. + Default: 'Install.ps1' +#> +function Install-ModuleFromLocal { + [CmdletBinding()] + param ( + [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)] + [String] $ModulePath, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $ModuleName, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [ValidateSet('ZIP', 'PSM1', 'PSD1', '')] # $script:PSGET_ZIP, $script:PSGET_PSM1 or $script:PSGET_PSD1 + [String] $Type, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $Destination = $global:PsGetDestinationModulePath, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $ModuleHash, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Global, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotImport, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $AddToProfile, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Update, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $PersistEnvironment, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $InstallWithModuleName, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotPostInstall, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $PostInstallHook + ) + process { + Write-Verbose 'Module will be installed from local path' + + $InstallWithModuleName = if ($InstallWithModuleName) { $InstallWithModuleName } else { $ModuleName } + if ($InstallWithModuleName) { + if (Test-ModuleInstalledAndImport -ModuleName:$InstallWithModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) { + return + } + } + + $tempFolderPath = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString()) + New-Item $tempFolderPath -ItemType Directory | Out-Null + Write-Debug "Temporary work directory created: $tempFolderPath" + + trap { Remove-Item -Path $tempFolderPath -Recurse -Force ; break } + + $newModulePath = Join-Path -Path $tempFolderPath -ChildPath 'module' + New-Item $newModulePath -ItemType Directory | Out-Null + + if (Test-Path -Path $ModulePath -PathType Leaf) { + $extension = (Get-Item $ModulePath).Extension + if ($extension -eq '.psm1') { + $Type = if ($Type) { $Type } else { $PSGET_PSM1 } + } elseif ($extension -eq '.zip') { + $Type = if ($Type) { $Type } else { $PSGET_ZIP } + } + + if ($Type -eq $PSGET_ZIP) { + Expand-ZipModule $ModulePath $newModulePath + } + else { + Copy-Item -Path $ModulePath -Destination $newModulePath + } + } + elseif (Test-Path -Path $ModulePath -PathType Container) { + Copy-Item -Path $ModulePath -Destination $newModulePath -Force -Recurse + } + else { + throw "ModulePath '$ModulePath' does not point to an module." + } + + $foundResult = Find-ModuleNameAndFolder -Path $newModulePath -ModuleName $ModuleName + + if (-not $PostInstallHook) { + $PostInstallHook = 'Install.ps1' + } + + Install-ModuleToDestination -ModuleName:$foundResult.ModuleName -InstallWithModuleName:$InstallWithModuleName -ModuleFolderPath:$foundResult.ModuleFolderPath -TempFolderPath:$tempFolderPath -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook + } +} + +<# + .SYNOPSIS + Install a module from a NuGet source. + + .PARAMETER NuGetPackageId + NuGet package name containing the module to install. + + .PARAMETER PackageVersion + Allows a specific version of the specified NuGet package to used, if not specified then the latest stable version will be used. + + .PARAMETER NugetSource + URL to the NuGet feed containing the package. + + .PARAMETER PreRelease + If PackageVersion is not specified, then this switch allows the latest prerelease package to be used. + + .PARAMETER PreReleaseTag + If PackageVersion is not specified, then this parameter allows the latest version of a particular prerelease tag to be used + + .PARAMETER Destination + When specified the module will be installed below this path. Defaults to '$global:PsGetDestinationModulePath' if defined. + + .PARAMETER ModuleHash + When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash. + + .PARAMETER Global + If set, attempts to install the module to the all users location in C:\Program Files\Common Files\Modules... + + NOTE: If the -Destination directory is specified, then -Global will only have an effect in combination with '-PersistEnvironment'. This is also the case if '$global:PsGetDestinationModulePath' is defined. + + .PARAMETER DoNotImport + Indicates that command should not import module after installation + + .PARAMETER AddToProfile + Adds Import-Module statement for installed module to the profile.ps1 + + .PARAMETER Update + Forces module to be updated + + .PARAMETER PersistEnvironment + If this switch is specified, the installation destination path will be added to either the User's PSModulePath environment variable or Machine's PSModulePath environment variable (if -Global specified) + + .PARAMETER InstallWithModuleName + Allows to specify the name of the module and override the ModuleName normally used. + NOTE: This parameter allows to install a module from the PsGet-Directory more than once and PsGet does not remember that this module is installed with a different name. + + .PARAMETER DoNotPostInstall + If defined, the PostInstallHook is not executed. + + .PARAMERTER PostInstallHook + Defines the name of a script inside the installed module folder which should be executed after installation. + Default: 'Install.ps1' +#> +function Install-ModuleFromNuGet { + [CmdletBinding()] + param ( + [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true)] + [ValidatePattern('^\w+([_.-]\w+)*$')] # regex from NuGet.PackageIdValidator._idRegex + [ValidateLength(1,100)] # maximum length from NuGet.PackageIdValidator.MaxPackageIdLength + [String] $NuGetPackageId, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $PackageVersion, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $NugetSource = 'https://nuget.org/api/v2/', + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $PreRelease, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $PreReleaseTag, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $Destination = $global:PsGetDestinationModulePath, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $ModuleHash, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Global, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotImport, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $AddToProfile, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $Update, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $PersistEnvironment, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $InstallWithModuleName, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [Switch] $DoNotPostInstall, + + [Parameter(ValueFromPipelineByPropertyName=$true)] + [String] $PostInstallHook + ) + process { + Write-Verbose 'Module will be installed from NuGet' + $InstallWithModuleName = if ($InstallWithModuleName) { $InstallWithModuleName } else { $NuGetPackageId } + + if (Test-ModuleInstalledAndImport -ModuleName:$InstallWithModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) { + return + } + + if (-not $PostInstallHook) { + $PostInstallHook = 'Install.ps1' + } + + try { + $result = Invoke-DownloadNugetPackage -NuGetPackageId $NuGetPackageId -PackageVersion $PackageVersion -Source $NugetSource -PreRelease:$PreRelease -PreReleaseTag $PreReleaseTag + Install-ModuleToDestination -ModuleName:$result.ModuleName -InstallWithModuleName:$InstallWithModuleName -ModuleFolderPath:$result.ModuleFolderPath -TempFolderPath:$result.TempFolderPath -Destination:$Destination -ModuleHash:$ModuleHash -Global:$Global -PersistEnvironment:$PersistEnvironment -DoNotImport:$DoNotImport -AddToProfile:$AddToProfile -Update:$Update -DoNotPostInstall:$DoNotPostInstall -PostInstallHook:$PostInstallHook + } + catch { + Write-Error $_.Exception.Message + return + } + } +} + +#endregion + +#region Internal Cmdlets +#region Module Installation +<# + .SYNOPSIS + Adds value to a "Path" type of environment variable (PATH or PSModulePath). Path type of variables munge the User and Machine values into the value for the current session. + + .PARAMETER Global + The System.EnvironmentVariableTarget of what type of environment variable to modify ("Machine","User" or "Session") + + .PARAMETER PathToAdd + The actual path to add to the environment variable + + .PARAMETER PersistEnvironment + If specified, will permanently store the variable in registry + + .EXAMPLE + AddPathToPSModulePath -Scope "Machine" -PathToAdd "$env:CommonProgramFiles\Modules" + + Description + ----------- + This command add the path "$env:CommonProgramFiles\Modules" to the Machine PSModulePath environment variable +#> +function Add-PathToPSModulePath { + [CmdletBinding()] + param ( + + [Parameter(Mandatory=$true)] + [string] $PathToAdd, + + [switch] $PersistEnvironment, + + [switch] $Global + ) + process { + $PathToAdd = ConvertTo-CanonicalPath -Path $PathToAdd + + if(-not $PersistEnvironment) { + if (-not ($env:PSModulePath.Contains($PathToAdd))) { + Write-Warning "Module install destination `"$PathToAdd`" is not included in the PSModulePath environment variable." + } + return + } + + $scope = 'User' + if ($Global) { + Write-Verbose 'The Machine environment variable PSModulePath will be modified.' + $scope = 'Machine' + } + + $pathValue = '' + [Environment]::GetEnvironmentVariable('PSModulePath', $scope) + + if (-not ($pathValue.Contains($PathToAdd))) { + if ($pathValue -eq '') { + Write-Debug "PSModulePath for scope '$scope' was read empty. Setting PowerShell default instead." + if ($scope -eq 'User') { + $pathValue = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath 'WindowsPowerShell\Modules' + } + else { + $pathValue = Join-Path -Path $PSHOME -ChildPath 'Modules' + } + } + + if (-not ($pathValue.Contains($PathToAdd))) { + $pathValue = "$pathValue;$PathToAdd" + } + + [Environment]::SetEnvironmentVariable('PSModulePath', $pathValue, $scope) + + Update-PSModulePath + + Write-Host """$PathToAdd"" is added to the PSModulePath environment variable" + } + else { + Write-Verbose """$PathToAdd"" already exists in PSModulePath environment variable" + } + } +} + +<# + .SYNOPSIS + Standardize the provided path. + + .DESCRIPTION + A simple routine to standardize path formats. + + .PARAMETER Path +#> +function ConvertTo-CanonicalPath { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [String] $Path + ) + process { + return [IO.Path]::GetFullPath(($Path.Trim())) + } +} + +<# + .SYNOPSIS + Find the module file in the given path. + + .PARAMETER Path + Path of module + + .PARAMETER ModuleName + Name of the Module +#> +function Get-ModuleFile { + [CmdletBinding()] + param( + [Parameter(Position=0, Mandatory=$true)] + [String] $Path, + + [String] $ModuleName = '*' + ) + process { + $Includes = Get-PossibleModuleFileNames -ModuleName $ModuleName + + # Sort by folder length ensures that we use one from root folder(Issue #12) + $DirectoryNameLengthProperty = @{ + E = { $_.DirectoryName.Length } + } + + # sort by Includes to give PSD1 preference over PSM1, etc + $IncludesPreferenceProperty = @{ + E = { + for ($Index = 0; $Index -lt $Includes.Length; $Index++) { + if ($_.Name -like $Includes[$Index]) { break } + } + $Index + } + } + + Get-ChildItem -Path $Path -Include $Includes -Recurse | + Where-Object { -not $_.PSIsContainer } | + Sort-Object -Property $DirectoryNameLengthProperty, $IncludesPreferenceProperty | + Select-Object -ExpandProperty FullName -First 1 + } +} + +<# + .SYNOPSIS + Get list of possible names for the module file. + + .PARAMETER ModuleName + Name of the module +#> +function Get-PossibleModuleFileNames { + [CmdletBinding()] + param( + [Parameter(Position=0, Mandatory=$true)] + [String] $ModuleName + ) + process { + 'psd1','psm1','ps1','dll','cdxml','xaml' | ForEach-Object { "$ModuleName`.$_" } + } +} + +<# + .SYNOPSIS + Search in the provided folder for a module, if possible with the provided name. + + .PARAMETER Path + Path to search in for the module. + + .PARAMETER ModuleName + ModuleName which is expected. +#> +function Find-ModuleNameAndFolder { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [String] $Path, + + [String] $ModuleName + ) + process { + if ($ModuleName) { + $moduleFile = Get-ModuleFile -Path $Path -ModuleName $ModuleName + if (-not $moduleFile) { + throw "Could not find a module with name '$ModuleName' in the provided file." + } + } + else { + $moduleFile = Get-ModuleFile -Path $Path + if (-not $moduleFile) { + throw 'Could not find any module in the provided file.' + } + $ModuleName = [IO.Path]::GetFileNameWithoutExtension($moduleFile) + } + + $moduleFolderPath = Split-Path -Path $moduleFile + + return @{ + ModuleName = $ModuleName + ModuleFolderPath = $moduleFolderPath + } + } +} + +<# + .SYNOPSIS + Import given modele + + .DESCRIPTION + Import given module with switch -Global (functions available to other modules) and avoid + a Powershell bug related to binary modules. + + .$ +#> +function Import-ModuleGlobally { + [CmdletBinding()] + param ( + [String] $ModuleName, + [String] $ModuleBase, + [Switch] $Force + ) + process { + Write-Verbose "Importing installed module '$ModuleName' from '$($installedModule.ModuleBase)'" + Import-Module -Name $ModuleBase -Global -Force:$Force + + $IdentityExtension = [System.IO.Path]::GetExtension((Get-ModuleFile -Path $ModuleBase -ModuleName $ModuleName)) + if ($IdentityExtension -eq '.dll') { + # import module twice for binary modules to workaround PowerShell bug: + # https://connect.microsoft.com/PowerShell/feedback/details/733869/import-module-global-does-not-work-for-a-binary-module + Import-Module -Name $ModuleBase -Global -Force:$Force + } + } +} + +<# + .SYNOPSIS + Download module from URL + + .DESCRIPTION + Download module from URL and try to interfere unknown parameter. + If download target is a zip-archive it will be extracted. + + Returns a map containing the TempFolderPath, ModuleFolderPath and ModuleName. + The TempFolderPath should be removed after processing the result. + + .PARAMETER DownloadUrl + URL to the module delivery file. + + .PARAMETER ModuleName + Name of the module. + + .PARAMETER Type + Type of the module delivery file. + + .PARAMETER Verb + Http method used for download. +#> +function Invoke-DownloadModuleFromWeb { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [String] $DownloadUrl, + + [String] $ModuleName, + + [String] $Type, + + [String] $Verb + ) + + $tempFolderPath = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ([Guid]::NewGuid().ToString()) + New-Item -Path $tempFolderPath -ItemType Directory | Out-Null + Write-Debug "Temporary work directory created: $tempFolderPath" + + # make certain that the tempFolder will be deleted if there is an error + trap { Remove-Item -Path $tempFolderPath -Recurse -Force; break } + + Write-Verbose "Downloading module from $DownloadUrl" + $client = (new-object Net.WebClient) + $client.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + $downloadFilePath = Join-Path -Path $tempfolderPath -ChildPath 'download' + if ($Verb -eq 'POST') { + $client.Headers['Content-type'] = 'application/x-www-form-urlencoded' + [IO.File]::WriteAllBytes($downloadFilePath, $client.UploadData($DownloadUrl, '')) + } + else { + $client.DownloadFile($DownloadUrl, $downloadFilePath) + } + + $candidateName = '{undefined}' + $contentDisposition = $client.ResponseHeaders['Content-Disposition'] + Write-Debug "Try to get module file name from content disposition header: Content-Disposition = '$contentDisposition'" + + if ($contentDisposition -match '\bfilename="?(?[^/]+)\.(?psm1|zip)"?') { + $candidateName = $Matches.name + $Type = if ($Type) { $Type } elseif ($Matches.ext -eq 'psm1') { $PSGET_PSM1 } elseif ($Matches.ext -eq 'zip') { $PSGET_ZIP } + } + else { + Write-Debug "Try to get module file name from url: '$DownloadUrl'" + if ($DownloadUrl -match '\b(?[^/]+)\.(?psm1|zip)[\#\?]*') { + $candidateName = $Matches.name + $Type = if ($Type) { $Type } elseif ($Matches.ext -eq 'psm1') { $PSGET_PSM1 } elseif ($Matches.ext -eq 'zip') { $PSGET_ZIP } + } + else { + $locationHeader = $client.ResponseHeaders['Location'] + Write-Debug "Check location header in case of redirect: '$locationHeader'" + if ($locationHeader -match '\b(?[^/]+)\.(?psm1|zip)[\#\?]*') { + $candidateName = $Matches.name + $Type = if ($Type) { $Type } elseif ($Matches.ext -eq 'psm1') { $PSGET_PSM1 } elseif ($Matches.ext -eq 'zip') { $PSGET_ZIP } + } + } + } + + Write-Debug "Invoke-DownloadModuleFromWeb: CandidateName = '$candidateName'" + + if (-not $Type) { + $contentType = $client.ResponseHeaders['Content-Type'] + Write-Debug "Download header Content-Type: '$contentType'" + if ($contentType -eq 'application/zip') { + $type = $PSGET_ZIP + } + # check downloaded file for the PKZip header + elseif ((Get-Item -Path $downloadFilePath).Length -gt 4) { + Write-Debug 'Search for PKZipHeader' + $knownPKZipHeader = 0x50, 0x4b, 0x03, 0x04 + $fileHeader = Get-Content -Path $downloadFilePath -Encoding Byte -TotalCount 4 + if ([System.BitConverter]::ToString($knownPKZipHeader) -eq [System.BitConverter]::ToString($fileHeader)) { + Write-Debug 'Found PKZipHeader => Type = ZIP' + $type = $PSGET_ZIP + } + else { + Write-Debug 'No PKZipHeader found => Type -ne ZIP' + } + } + + if (-not $Type) { + Write-Debug 'If its most likely no zip it has to be an PSM1 file.' + $Type = $PSGET_PSM1 + } + } + + $moduleFolderPath = Join-Path -Path $tempFolderPath -ChildPath 'module' + New-Item -Path $moduleFolderPath -ItemType Directory | Out-Null + + switch ($Type) { + $PSGET_ZIP { + $zipFilePath = $downloadFilePath + '.zip' + Move-Item -Path $downloadFilePath -Destination $zipFilePath + Expand-ZipModule -Path $zipFilePath -Destination $moduleFolderPath + } + $PSGET_PSM1 { + if (-not $ModuleName) { + if ($candidateName -eq '{undefined}') { + throw 'Cannot guess module name. Try specifying ModuleName argument!' + } + $ModuleName = $candidateName + } + + $psmFilePath = Join-Path -Path $moduleFolderPath -ChildPath "$ModuleName.psm1" + Move-Item -Path $downloadFilePath -Destination $psmFilePath + } + default { + throw "Type $Type is not supported yet" + } + } + + $foundResult = Find-ModuleNameAndFolder -Path $moduleFolderPath -ModuleName $ModuleName + + Write-Debug "Invoke-DownloadModuleFromWeb: ModuleName = '$ModuleName'" + + return @{ + TempFolderPath = $tempFolderPath + ModuleFolderPath = $foundResult.ModuleFolderPath + ModuleName = $foundResult.ModuleName + } +} + +<# + .SYNOPSIS + Install the provided module into the defined destination. + + .DESCRIPTION + Install the module inside of the provided directory into the defined destination + and perform the following steps: + + * Rename module if requestes by provided InstallWithModuleName + * If a ModuleHash is provided, check if it matches. + * Add the destination path to the PSModulePath if necessary (depends on provided parameters) + * Place the conventions-matching module folder in the destination folder + * Import the module if necessary + * Add the profile import to profile if necessary + + .PARAMETER ModuleName + The name of the module. + + .PARAMETER InstallWithModuleName + The name the module should get. + + .PARAMETER ModuleFolderPath + The path to the module data, which contains the module main file, named according to ModuleName + + .PARAMETER TempFolderPath + TempPath used by PsGet for doing the work. Contains the ModuleFolderPath and will be deleted after processing, + + .PARAMETER Destination + Path to which the module will be installed. + + .PARAMETER ModuleHash + When ModuleHash is specified the chosen module will only be installed if its contents match the provided hash. + + .PARAMETER Global + Influence the PSModulePath changes and profile changes. + + .PARAMETER PersistEnvironment + Defines if the PSModulePath changes should be persistent. + + .PARAMETER DoNotImport + Defines if the installed module should be imported. + + .PARAMETER AddToProfile + Defines if an 'Import-Module' statement should be added to the profile. + + .PARAMETER Update + Defines if an already existing folder in the target may be deleted for installation of the module. + + .PARAMETER DoNotPostInstall + If defined, the PostInstallHook is not executed. + + .PARAMERTER PostInstallHook + Defines the name of a script inside the installed module folder which should be executed after installation. +#> +function Install-ModuleToDestination { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [String] $ModuleName, + + [Parameter(Mandatory=$true)] + [String] $ModuleFolderPath, + + [Parameter(Mandatory=$true)] + [String] $TempFolderPath, + + [Parameter(Mandatory=$true)] + [String] $Destination, + + [String] $InstallWithModuleName, + + [String] $ModuleHash, + + [Switch] $Global, + + [Switch] $PersistEnvironment, + + [Switch] $DoNotImport, + + [Switch] $AddToProfile, + + [Switch] $Update, + + [Switch] $DoNotPostInstall, + + [String] $PostInstallHook + ) + process { + # Make certain the temp folder is deleted + trap { Remove-Item -Path $TempFolderPath -Recurse -Force; break } + + $InstallWithModuleName = if ($InstallWithModuleName) { $InstallWithModuleName } else { $ModuleName } + # Case: no $InstallWithModuleName and module name interfered from install files + if (Test-ModuleInstalledAndImport -ModuleName:$InstallWithModuleName -Destination:$Destination -Update:$Update -DoNotImport:$DoNotImport -ModuleHash:$ModuleHash) { + Remove-Item -Path $TempFolderPath -Recurse -Force + return + } + + $moduleFilePath = Get-ModuleFile -Path $ModuleFolderPath -ModuleName $ModuleName + # sanity checks + if (-not $moduleFilePath) { + throw 'BUG! Module installation failed in step Install-ModuleToDestination. Please report this issue including your command line.' + } + if ($ModuleFolderPath -ne (Split-Path -Path $moduleFilePath)) { + throw 'BUG! Module installation failed in step Install-ModuleToDestination. Please report this issue including your command line.' + } + + if ($InstallWithModuleName -ne $ModuleName) { + Rename-Item -Path $moduleFilePath -NewName ($InstallWithModuleName + (Get-Item $moduleFilePath).Extension) + } + + $targetFolderPath = Join-Path -Path $Destination -ChildPath $InstallWithModuleName + + if ($ModuleHash) { + Write-Verbose 'Ensure that the hash of the module matches the specified hash' + + $newModuleHash = Get-PsGetModuleHash -Path $ModuleFolderPath + Write-Verbose "Hash of module in '$ModuleFolderPath' is: $newModuleHash" + if ($ModuleHash -ne $newModuleHash) { + throw 'Module contents do not match specified module hash. Ensure the expected hash is correct and the module source is trusted.' + } + + if ( Test-Path $targetFolderPath ) { + Write-Verbose 'Module already exists in destination path. Check if hash in destination is correct. If not replace with to be installed module.' + $destinationModuleHash = Get-PsGetModuleHash -Path $targetFolderPath + if ($destinationModuleHash -ne $ModuleHash ) { + $Update = $true + } + } + } + + #Add the Destination path to the User or Machine environment + Add-PathToPSModulePath -PathToAdd:$Destination -PersistEnvironment:$PersistEnvironment -Global:$Global + + if (-not (Test-Path $targetFolderPath)) { + New-Item $targetFolderPath -ItemType Directory -ErrorAction Continue -ErrorVariable FailMkDir | Out-Null + ## Handle the error if they asked for -Global and don't have permissions + if ($FailMkDir -and @($FailMkDir)[0].CategoryInfo.Category -eq 'PermissionDenied') { + throw "You do not have permission to install a module to '$Destination'. You may need to be elevated." + } + Write-Verbose "Create module folder at $targetFolderPath" + } + + Write-Debug 'Empty existing module folder before copying new files.' + Get-ChildItem -Path $targetFolderPath -Force | Remove-Item -Force -Recurse -ErrorAction Stop + + Write-Debug 'Copy module files to destination folder' + Get-ChildItem -Path $ModuleFolderPath | Copy-Item -Destination $targetFolderPath -Force -Recurse + + if (-not $DoNotPostInstall) { + Write-Verbose "PostInstallHook $PostInstallHook" + if ($PostInstallHook -like '*.ps1') { + $postInstallScript = Join-Path -Path $targetFolderPath -ChildPath $PostInstallHook + if (Test-Path -Path $postInstallScript -PathType Leaf) { + Write-Verbose "'$PostInstallHook' found in module. Let's execute it." + & $postInstallScript + } + else { + Write-Verbose "PostInstallHook '$PostInstallHook' not found." + } + } + } + + $isDestinationInPSModulePath = $env:PSModulePath.Contains($Destination) + if ($isDestinationInPSModulePath) { + if (-not (Get-Module $ModuleName -ListAvailable)) { + throw 'For some unexpected reasons module was not installed.' + } + } + else { + if (-not (Get-ModuleFile -Path $targetFolderPath)) { + throw 'For some unexpected reasons module was not installed.' + } + } + + if ($Update) { + Write-Host "Module $ModuleName was successfully updated." -Foreground Green + } + else { + Write-Host "Module $ModuleName was successfully installed." -Foreground Green + } + + if (-not $DoNotImport) { + Import-ModuleGlobally -ModuleName:$ModuleName -ModuleBase:$targetFolderPath -Force:$Update + } + + if ($isDestinationInPSModulePath -and $AddToProfile) { + # WARNING $Profile is empty on Win2008R2 under Administrator + if ($PROFILE) { + if (-not (Test-Path $PROFILE)) { + Write-Verbose "Creating PowerShell profile...`n$PROFILE" + New-Item $PROFILE -Type File -Force -ErrorAction Stop + } + + if (Select-String $PROFILE -Pattern "Import-Module $ModuleName") { + Write-Verbose "Import-Module $ModuleName command already in your profile" + } + else { + $signature = Get-AuthenticodeSignature -FilePath $PROFILE + + if ($signature.Status -eq 'Valid') { + Write-Error "PsGet cannot modify code-signed profile '$PROFILE'." + } + else { + Write-Verbose "Add Import-Module $ModuleName command to the profile" + "`nImport-Module $ModuleName" | Add-Content $PROFILE + } + } + } + + } + + Write-Debug "Cleanup temporary work folder '$TempFolderPath'" + Remove-Item -Path $TempFolderPath -Recurse -Force + } +} + +<# + .SYNOPSIS + Test if module is installed and import it then. + + .DESCRIPTION + Test if module with provided name is installed in the target destination. + If it is installed, it will be imported. Returns '$true' if installed. + + .PARAMETER ModuleName + Name of the module + + .PARAMETER Destination + Installation destination + + .PARAMETER Update + If 'Update'-switch is set, this returns always '$true'. + + .PARAMETER DoNotImport + Switch suppress the import of module. + + .PARAMETER ModuleHash + If a hash is provided an installed module will only be accepted as installed if the hash match. +#> +function Test-ModuleInstalledAndImport { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [String] $ModuleName, + + [Parameter(Mandatory=$true)] + [String] $Destination, + + [Switch] $Update, + + [Switch] $DoNotImport, + + [String] $ModuleHash + ) + process { + if ($Update) { + #TODO: This implementation is more like the old -Force flag, because this will force an installation also if no installation in destination exists. + Write-Verbose "Ignoring if module with name '$ModuleName' is already installed because of update mode." + return $false + } + + $installedModule = Get-Module -Name $ModuleName -ListAvailable + + if ($installedModule) { + if ($installedModule.Count -gt 1) { + $targetModule = $installedModule | Where-Object { (ConvertTo-CanonicalPath -Path (Split-Path $_.ModuleBase)) -eq $Destination } | Select-Object -First 1 + + if (-not $targetModule) { + Write-Warning "Module with name '$ModuleName' was not found in '$Destination'. But it was found in:`n $($installedModule.ModuleBase | Format-List | Out-String)" + return $false + } + + Write-Warning "The module '$ModuleName' was installed at more then one location. Installed paths:`n`t$($installedModule.ModuleBase | Format-List | Out-String)`n'$($firstInstalledModule.ModuleBase)' is the searched destination." + $installedModule = $targetModule + } + elseif ((Split-Path $installedModule.ModuleBase) -ne $Destination) { + Write-Verbose "Module with name '$ModuleName' was found in '$($installedModule.ModuleBase)' but not in '$Destination'." + return $false + } + } + else { + $candidateModulePath = Join-Path -Path $Destination -ChildPath $ModuleName + $possibleModuleFileNames = Get-PossibleModuleFileNames -ModuleName $ModuleName + + if (Test-Path -Path $candidateModulePath\* -Include $possibleModuleFileNames -PathType Leaf) { + Write-Verbose "Module with name '$ModuleName' found in '$Destination' (note: destination is not in PSModulePath)" + $installedModule = @{ ModuleBase = $CandidateModulePath } + } + else { + Write-Verbose "Module with name '$ModuleName' is not installed." + return $false + } + } + + if ($ModuleHash) { + $installedModuleHash = Get-PsGetModuleHash -Path $installedModule.ModuleBase + Write-Verbose "Hash of module in '$($installedModule.ModuleBase)' is: $InstalledModuleHash" + if ($ModuleHash -ne $installedModuleHash) { + Write-Verbose "Expected '$ModuleHash' but calculated '$installedModuleHash'." + return $false + } + } + + Write-Verbose "'$ModuleName' already installed. Use -Update if you need update" + + if ($DoNotImport -eq $false) { + Import-ModuleGlobally -ModuleName $ModuleName -ModuleBase $installedModule.ModuleBase -Force:$Update + } + + return $true + } +} + +<# + .SYNOPSIS + Extract the content of the referenced zip file to the defind destination + + .PARAMATER Path + Path to a zip file with the file extension '.zip' + + .Parameter Destination + Path to which the zip content is extracted +#> +function Expand-ZipModule { + [CmdletBinding()] + param ( + [Parameter(Position=0, Mandatory=$true)] + [String] $Path, + + [Parameter(Position=1, Mandatory=$true)] + [String] $Destination + ) + process { + Write-Debug "Unzipping $Path to $Destination..." + + # Check if powershell v3+ and .net v4.5 is available + $netFailed = $true + if ( $PSVersionTable.PSVersion.Major -ge 3 -and (Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4' -Recurse | Get-ItemProperty -Name Version | Where-Object { $_.Version -like '4.5*' }) ) { + Write-Debug 'Attempting unzip using the .NET Framework...' + + try { + [System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") + [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $Destination) + $netFailed = $false + } + catch { + } + } + + if ($netFailed) { + try { + Write-Debug 'Attempting unzip using the Windows Shell...' + $shellApp = New-Object -Com Shell.Application + $shellZip = $shellApp.NameSpace([String]$Path) + $shellDest = $shellApp.NameSpace($Destination) + $shellDest.CopyHere($shellZip.items()) + } + catch { + $shellFailed = $true + } + } + + # if failure already registered or no result + if (($netFailed -and $shellFailed) -or ((Get-ChildItem $Destination | Measure-Object | Where-Object { $_.Count -eq 0}))) { + Write-Warning 'We were unable to decompress the downloaded module. This tends to mean both of the following are true:' + Write-Warning '1. You''ve disabled Windows Explorer Zip file integration or are running on Windows Server Core.' + Write-Warning '2. You don''t have the .NET Framework 4.5 installed.' + Write-Warning 'You''ll need to correct at least one of the above issues depending on your installation to proceed.' + throw 'Unable to unzip downloaded module file!' + } + } +} + +<# + .SYNOPSIS + Update '$env:PSModulePath' from 'User' and 'Machine' scope envrionment variables +#> +function Update-PSModulePath { + process { + # powershell default + $psModulePath = "$env:ProgramFiles\WindowsPowershell\Modules\" + + $machineModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine') + if (-not $machineModulePath) { + # powershell default + $machineModulePath = Join-Path -Path $PSHOME -ChildPath 'Modules' + } + + $userModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'User') + if (-not $userModulePath) { + # powershell default + $userModulePath = Join-Path -Path ([Environment]::GetFolderPath('MyDocuments')) -ChildPath 'WindowsPowerShell\Modules' + } + + $newSessionValue = "$userModulePath;$machineModulePath;$psModulePath" + + #Set the value in the current process + [Environment]::SetEnvironmentVariable('PSModulePath', $newSessionValue, 'Process') + } +} +#endregion + +#region NuGet Handling +<# + .SYNOPSIS + Download a module of type NuGet package + + .PARAMETER NuGetPackageId + NuGet package id + + .PARAMETER PackageVersion + Specific version to be installed. If not defined, install newest. + + .PARAMETER Source + NuGet source url + + .PARAMETER PreRelease + If no PackageVersion is defined, may PreReleases be used? + + .PARAMETER PreReleaseTag + If PreReleases may be used, also use prereleases of a special tag? +#> +function Invoke-DownloadNuGetPackage { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [String] $NuGetPackageId, + + [String] $PackageVersion, + + [Parameter(Mandatory=$true)] + [String] $Source, + + [Switch] $PreRelease, + + [String] $PreReleaseTag + ) + process { + $WebClient = New-Object -TypeName System.Net.WebClient + $WebClient.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + + if (-not $Source.EndsWith('/')) { + $Source += '/' + } + + Write-Verbose "Querying '$Source' repository for package with Id '$NuGetPackageId'" + $Url = "{1}Packages()?`$filter=tolower(Id)+eq+'{0}'&`$orderby=Id" -f $NuGetPackageId.ToLower(), $Source + Write-Debug "NuGet query url: $Url" + + try { + $XmlDoc = [xml]$WebClient.DownloadString($Url) + } + catch { + throw "Unable to download from NuGet feed: $($_.Exception.InnerException.Message)" + } + + if ($PackageVersion) { + # version regexs can be found in the NuGet.SemanticVersion class + $Entry = $XmlDoc.feed.entry | + Where-Object { $_.properties.Version -eq $PackageVersion } | + Select-Object -First 1 + } + else { + $Entry = Find-LatestNugetPackageFromFeed -Feed:$XmlDoc.feed.entry -PreRelease:$PreRelease -PreReleaseTag:$PreReleaseTag + } + + if ($Entry) { + $PackageVersion = $Entry.properties.Version + Write-Verbose "Found NuGet package version '$PackageVersion'" + } + else { + throw ("Cannot find NuGet package '$NuGetPackageId $PackageVersion' [PreRelease='{0}', PreReleaseTag='{1}']" -f $PreRelease, $PreReleaseTag) + } + + $DownloadUrl = $Entry.content.src + Write-Verbose "Downloading NuGet package from '$DownloadUrl'" + $DownloadResult = Invoke-DownloadModuleFromWeb -DownloadUrl:$DownloadUrl -ModuleName:$NugetPackageId + return $DownloadResult + } +} + +<# + .SYNOPSIS + Find the latest release in the provided NuGet feed for the NuGet package id. + + .PARAMETER Feed + Xml feed node for NuGet package + + .PARAMETER PreRelease + If no PackageVersion is defined, may PreReleases be used? + + .PARAMETER PreReleaseTag + If PreReleases may be used, also use prereleases of a special tag? +#> +function Find-LatestNugetPackageFromFeed { + [CmdletBinding()] + param + ( + [Object[]] $Feed, + + [Switch] $PreRelease, + + [String] $PreReleaseTag + ) + process { + # From NuGet.SemanticVersion - https://github.com/Haacked/NuGet/blob/master/src/Core/SemanticVersion.cs + $semVerRegex = "^(?\d+(\s*\.\s*\d+){0,3})(?-[a-z][0-9a-z-]*)?$" + $semVerStrictRegex = "^(?\d+(\.\d+){2})(?-[a-z][0-9a-z-]*)?$" + + # find only stable versions + $stableRegex = "^(\d+(\s*\.\s*\d+){0,3})?$" + # find stable and prerelease versions + $preReleaseRegex = "^(\d+(\s*\.\s*\d+){0,3})(-[a-z][0-9a-z-]*)?$" + # find only a specific prerelease versions + $specificPreReleaseRegex = "^(\d+(\s*\.\s*\d+){{0,3}}-{0}[0-9a-z-]*)?$" -f $preReleaseTag + + # Set the required search expression + $searchRegex = $stableRegex + if ($preRelease) { $searchRegex = $preReleaseRegex } + if ($preReleaseTag) { $searchRegex = $specificPreReleaseRegex } + + $packages = $feed | Where-Object { + + ($_.properties.Version) -match $searchRegex + } + + return ($packages | Select -Last 1) + } +} + +#endregion + +#region Module Hashing +<# + .SYNOPSIS + Calculate a hash for the given file + + .PARAMETER Path + File path for hasing +#> +function Get-FileHash { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName = $true)] + [Alias('FullName')] + [String] $Path + ) + begin { + $Algorithm = New-Object -TypeName System.Security.Cryptography.SHA256Managed + } + process { + if (-not (Test-Path -Path $Path -PathType Leaf)) { + Write-Error "Cannot find file: $Path" + return + } + + $Stream = [System.IO.File]::OpenRead($Path) + try { + $HashBytes = $Algorithm.ComputeHash($Stream) + [BitConverter]::ToString($HashBytes) -replace '-','' + } + finally { + $Stream.Close() + } + } +} + +<# + .SYNOPSIS + Calculate a hash for the given directory. + + .PARAMETER Path + Path to the folder which should be hashed. +#> +function Get-FolderHash { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [String] $Path + ) + process { + if (-not (Test-Path -Path $Path -PathType Container)) { + throw "Cannot find folder: $Path" + } + + $Path = $Path + '\' -replace '\\\\$','\\' + $PathPattern = '^' + [Regex]::Escape($Path) + + $ChildHashes = Get-ChildItem -Path $Path -Recurse -Force | + Where-Object { -not $_.PSIsContainer } | + ForEach-Object { + New-Object -TypeName PSObject -Property @{ + RelativePath = $_.FullName -replace $PathPattern, '' + Hash = Get-FileHash -Path $_.FullName + } + } + + $Text = @($ChildHashes | + Sort-Object -Property RelativePath | + ForEach-Object { + '{0} {1}' -f $_.Hash, $_.RelativePath + }) -join '`r`n' + + Write-Debug "TEXT>$Text