-
-
Save KirkMunro/131308abfb2d857bea40 to your computer and use it in GitHub Desktop.
[CmdletBinding()] | |
[OutputType([System.Management.Automation.PSModuleInfo])] | |
param( | |
# The name of the module to install from GitHub. | |
[Parameter(Position=0, Mandatory=$true)] | |
[ValidateNotNullOrEmpty()] | |
[System.String[]] | |
$ModuleName, | |
# The scope from which the module should be discoverable. | |
[Parameter(Position=1)] | |
[ValidateNotNullOrEmpty()] | |
[ValidateSet('CurrentUser','AllUsers')] | |
[System.String] | |
$Scope = 'AllUsers', | |
# The GitHub user name where the module was released. | |
[Parameter(Position=2)] | |
[ValidateNotNullOrEmpty()] | |
[System.String] | |
$GitHubUserName = 'KirkMunro', | |
# The branch from which you want to download source code. | |
[Parameter(Position=2)] | |
[ValidateNotNullOrEmpty()] | |
[System.String] | |
$Branch = 'release' | |
) | |
try { | |
foreach ($item in $ModuleName) { | |
#region Extract the GUID from the hosted module manifest. | |
# Create a temporary file name | |
$manifestPath = [System.IO.Path]::GetTempFileName() -replace '\.tmp$','.psd1' | |
try { | |
Write-Progress -Activity "Installing ${item}" -Status "Downloading the manifest for module ${item}." | |
# Download the module manifest | |
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/${GitHubUserName}/${item}/${Branch}/${item}.psd1" -OutFile $manifestPath | |
# Unblock the downloaded manifest | |
Unblock-File -LiteralPath $manifestPath | |
# Identify the module GUID from the manifest | |
$manifestContent = Get-Content -LiteralPath $manifestPath -Raw | |
$manifestScriptBlock = [System.Management.Automation.ScriptBlock]::Create($manifestContent) | |
$manifestHashtableAst = $manifestScriptBlock.Ast.Find({$args[0] -is [System.Management.Automation.Language.HashtableAst]},$false) | |
if (-not $manifestHashtableAst) { | |
$message = "The manifest for module '${item}' does not appear to be a manifest at all. No hashtable was found in the '${item}.psd1' file." | |
$itemNotFoundException = New-Object -TypeName System.Management.Automation.ItemNotFoundException -ArgumentList $message | |
$errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $itemNotFoundException,$itemNotFoundException.GetType().Name,'ObjectNotFound',$item | |
throw $errorRecord | |
} | |
if ($PSVersionTable.PSVersion -ge [System.Version]'5.0.10514.6') { | |
$guidEntryTuple = $manifestHashtableAst.KeyValuePairs | Where-Object {$_.Item1.SafeGetValue() -eq 'Guid'} | |
$moduleGuid = $guidEntryTuple.Item2.SafeGetValue() -as [System.Guid] | |
} else { | |
$moduleGuid = $null | |
$guidEntryTuple = $manifestHashtableAst.KeyValuePairs | Where-Object {($_.Item1 -is [System.Management.Automation.Language.StringConstantExpressionAst]) -and ($_.Item1.Value -eq 'Guid')} | |
if ($guidAst = $guidEntryTuple.Item2.Find({$args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]},$false)) { | |
$moduleGuid = $guidAst.Value -as [System.Guid] | |
} | |
} | |
} catch [System.Net.WebException] { | |
if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) { | |
$message = "The manifest for module '${item}' was not found in the GitHub repository for user '${GitHubUserName}' on the '${Branch}' branch. Please verify your module name is correct and then try again." | |
$itemNotFoundException = New-Object -TypeName System.Management.Automation.ItemNotFoundException -ArgumentList $message | |
$errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $itemNotFoundException,$itemNotFoundException.GetType().Name,'ObjectNotFound',$item | |
throw $errorRecord | |
} | |
} finally { | |
# Remove the manifest that was downloaded | |
if (Test-Path -LiteralPath $manifestPath) { | |
Remove-Item -LiteralPath $manifestPath | |
} | |
} | |
# Raise an error if the GUID was not found in the manifest | |
if ($moduleGuid -eq $null) { | |
[System.String]$message = "Failed to identify the GUID for hosted module ${item}." | |
[System.Management.Automation.ItemNotFoundException]$exception = New-Object -TypeName System.Management.Automation.ItemNotFoundException -ArgumentList $message | |
[System.Management.Automation.ErrorRecord]$errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception,'ItemNotFoundException',([System.Management.Automation.ErrorCategory]::ObjectNotFound),$manifestPath | |
throw $errorRecord | |
} | |
#endregion | |
#region Make sure that multiple installs or loaded assemblies won't prevent the installation from succeeding. | |
# If there are multiple instances of the module we want to install/upgrade installed already, raise an error | |
Write-Progress -Activity "Installing ${item}" -Status "Looking for an installed ${item} module." | |
$module = Get-Module -ListAvailable | Where-Object {$_.Guid -eq $moduleGuid} | |
if ($module -is [System.Array]) { | |
[System.String]$message = "More than one version of ${item} is installed on this system. Manually remove the old versions and then try again." | |
[System.Management.Automation.SessionStateException]$exception = New-Object -TypeName System.Management.Automation.SessionStateException -ArgumentList $message | |
[System.Management.Automation.ErrorRecord]$errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception,'SessionStateException',([System.Management.Automation.ErrorCategory]::InvalidOperation),$module | |
throw $errorRecord | |
} | |
# Check to see if there are any assemblies loaded in an existing module folder before continuing | |
if ($module -and | |
($assemblies = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object {$_.Location -and $_.Location.StartsWith($module.ModuleBase)})) { | |
[System.String]$message = "It appears that the ${item} module was loaded at least once during this session and that an assembly it contains is still loaded. You must open a new PowerShell session where ${item} has not been loaded in order to upgrade the existing module." | |
[System.Management.Automation.SessionStateException]$exception = New-Object -TypeName System.Management.Automation.SessionStateException -ArgumentList $message | |
[System.Management.Automation.ErrorRecord]$errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception,'SessionStateException',([System.Management.Automation.ErrorCategory]::InvalidOperation),$assemblies | |
throw $errorRecord | |
} | |
#endregion | |
#region Identify the parent folder where the module will be installed. | |
Write-Progress -Activity "Installing ${item}" -Status 'Identifying the target modules folder.' | |
if ($Scope -eq 'AllUsers') { | |
$modulesFolder = Join-Path -Path ([System.Environment]::GetFolderPath('ProgramFiles')) -ChildPath WindowsPowerShell\Modules | |
# If we're on PowerShell 3.0, make sure that the All Users modules folder is | |
# in PSModulePath in the right spot if it is not already present. | |
if (($PSVersionTable.PSVersion -lt [System.Version]'4.0') -and | |
(-not (@($env:PSModulePath -split ';') -match "^$([System.Text.RegularExpressions.Regex]::Escape($modulesFolder))\\?$"))) { | |
Write-Progress -Activity "Installing ${item}" -Status 'Adding the All Users modules folder to the PSModulePath environment variable.' | |
$environmentVariableTarget = [System.EnvironmentVariableTarget]::Machine | |
if ($systemPSModulePath = [System.Environment]::GetEnvironmentVariable('PSModulePath',$environmentVariableTarget) -as [System.String]) { | |
$systemPSModuleList = [System.Collections.ArrayList]@($systemPSModulePath -split ';') | |
if ($systemPSModuleList.Count -gt 1) { | |
$systemPSModuleList.Insert(0,$modulesFolder) | |
} else { | |
$systemPSModuleList.Add($modulesFolder) | |
} | |
$systemPSModulePath = $systemPSModuleList -join ';' | |
[System.Environment]::SetEnvironmentVariable('PSModulePath',$systemPSModulePath,$environmentVariableTarget) | |
} | |
$psModuleList = [System.Collections.ArrayList]@($env:PSModulePath -split ';') | |
if ($psModuleList.Count -gt 2) { | |
$psModuleList.Insert(1,$modulesFolder) | |
} else { | |
$psModuleList.Add($modulesFolder) | |
} | |
$env:PSModulePath = $psModuleList -join ';' | |
} | |
} else { | |
$modulesFolder = Join-Path -Path ([System.Environment]::GetFolderPath('MyDocuments')) -ChildPath WindowsPowerShell\Modules | |
} | |
# Create the modules folder if it does not exist | |
if (-not (Test-Path -LiteralPath $modulesFolder)) { | |
Write-Progress -Activity "Installing ${item}" -Status 'Creating modules folder.' | |
New-Item -Path $modulesFolder -ItemType Directory -ErrorAction Stop > $null | |
} | |
#endregion | |
#region Download the module and extract it into the modules folder. | |
# Remove any previously extracted zip file contents from the modules folder (these | |
# may have accidentally been left behind before, so we need to clean them up first) | |
Join-Path -Path $modulesFolder -ChildPath "${GitHubUserName}-${item}-*" | Remove-Item -Recurse -ErrorAction Stop | |
try { | |
# Download and unblock the latest release from GitHub | |
Write-Progress -Activity "Installing ${item}" -Status "Downloading the latest version of ${item}." | |
$zipFilePath = Join-Path -Path $modulesFolder -ChildPath "${item}.zip" | |
$response = Invoke-WebRequest -Uri "https://github.com/${GitHubUserName}/${item}/zipball/${Branch}" -ErrorAction Stop | |
[System.IO.File]::WriteAllBytes($zipFilePath, $response.Content) | |
Unblock-File -LiteralPath $zipFilePath -ErrorAction Stop | |
# Extract the contents of the downloaded zip file into the modules folder | |
Write-Progress -Activity "Installing ${item}" -Status "Extracting the ${item} zip file contents." | |
# Check to see if we have the System.IO.Compression.FileSystem assembly installed. | |
# This comes as part of .NET 4.5 and later. | |
try { | |
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue | |
} catch { | |
} | |
if ('System.IO.Compression.ZipFile' -as [System.Type]) { | |
# If we have .NET 4.5 installed, use the ExtractToDirectory static method | |
[System.IO.Compression.ZipFile]::ExtractToDirectory($zipFilePath, $modulesFolder) | |
} else { | |
# Otherwise, use the CopyHere COM method (this is significantly slower) | |
$shell = New-Object -ComObject Shell.Application | |
$zip = $shell.NameSpace($zipFilePath) | |
foreach($item in $zip.items()) { | |
$shell.Namespace($modulesFolder).CopyHere($item) | |
} | |
} | |
} catch [System.Net.WebException] { | |
if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) { | |
$message = "The '${item}' module was not found in the GitHub repository for user '${GitHubUserName}' on the '${Branch}' branch. Please verify your module name is correct and then try again." | |
$itemNotFoundException = New-Object -TypeName System.Management.Automation.ItemNotFoundException -ArgumentList $message | |
$errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $itemNotFoundException,$itemNotFoundException.GetType().Name,'ObjectNotFound',$item | |
throw $errorRecord | |
} | |
} finally { | |
# Remove the downloaded zip file | |
Write-Progress -Activity "Installing ${item}" -Status "Removing the ${item} zip file." | |
Remove-Item -LiteralPath $zipFilePath | |
} | |
#endregion | |
#region Remove the old module version if one exists. | |
if ($module) { | |
Write-Progress -Activity "Installing ${item}" -Status "Unloading and removing the installed ${item} module." | |
# Unload the module if it is currently loaded. | |
if ($loadedModule = Get-Module | Where-Object {$_.Guid -eq $module.Guid}) { | |
$loadedModule | Remove-Module -ErrorAction Stop | |
} | |
# Remove the currently installed module. | |
Remove-Item -LiteralPath $module.ModuleBase -Recurse -Force -ErrorAction Stop | |
} | |
#endregion | |
#region Rename the extracted zip file contents folder as the module name and return the module to the caller. | |
# Rename the extracted zip file contents folder as the module name | |
Write-Progress -Activity "Installing ${item}" -Status "Installing the new ${item} module." | |
Join-Path -Path $modulesFolder -ChildPath "${GitHubUserName}-${item}-*" ` | |
| Get-Item ` | |
| Sort-Object -Property LastWriteTime -Descending ` | |
| Select-Object -First 1 ` | |
| Rename-Item -NewName $item | |
# Now return the updated module to the caller | |
Get-Module -ListAvailable -Name $item | |
#endregion | |
} | |
} catch { | |
$PSCmdlet.ThrowTerminatingError($_) | |
} |
Thank you for pointing out that bug Oliver. I just published a fix, but I didn't use the fork you created because it requires invocation of an arbitrary file (the manifest for the module, which really could contain anything). The update I made uses AST to identify the manifest in the hashtable and then SafeGetValue to find the GUID entry and it's appropriate value. This approach will safely and reliably identify the module GUID, and it will throw an exception for modules with hashtables that contain malicious keys or GUID values. See lines 42-52 if you want to see how this logic works.
I just published a second update to this fix. Turns out that SafeGetValue is a recent addition to the AST classes in PowerShell, so broke things in downlevel versions of PowerShell. I modified my changes yesterday such that SafeGetValue is only used if PowerShell is version 5.0 or later. Otherwise, I pull the value of the GUID that I need using other methods available in the AST.
I found out from @lipkau that SafeGetValue is not in some of the earlier pre-release builds of PowerShell 5.0. It is definitely in the version that ships on Windows 10 though (10.0.10586), as well as the version that is included in WMF5 RC (10.0.10514.6). With that in mind, I just bumped the greater than or equal to version check from '5.0' to '5.0.10514.6'. If you're working with an earlier version of PowerShell 5, you should be updating your WMF package to the RC or RTM (if the RTM is available). Regardless of the version, though, this should ensure that the script just works.
Any chance line 190 could be modified to check the file exists before trying to delete it?
PS C:\Users\Administrator> & ([scriptblock]::Create((iwr -uri http://tinyurl.com/Install-GitHubHostedModule).Content)) -GitHubUserName Positronic-IO -ModuleName PSImaging -Branch 'master'
Remove-Item : Cannot find path 'C:\Program Files\WindowsPowerShell\Modules\PSImaging.zip' because it does not exist.
At line:190 char:13
+ Remove-Item -LiteralPath $zipFilePath
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Program File...s\PSImaging.zip:String) [Remove-Item], ItemNotFoundEx
ception
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.RemoveItemCommand
Hey.
I forked this to change how the GUID of the manifest is parsed as the manifest can contain more matches in the
ModuleList
declaration.here is an example: https://github.com/mattifestation/PowerShellArsenal/blob/master/PowerShellArsenal.psd1
the changes are listed here: https://gist.github.com/lipkau/ac2ee75fb427870daeb4/6440252bde10e9a59146f498d843aa60df6b2acb