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/Cmder.exe b/Cmder.exe deleted file mode 100644 index f5cab6d..0000000 Binary files a/Cmder.exe and /dev/null differ diff --git a/README.md b/README.md index 1fe4610..c3e8067 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The main advantage of Cmder is portability. It is designed to be totally self-co 1. Download the latest release 1. Extract -1. (optional) Place files into `bin` folder, it will be injected into your PATH. +1. (optional) Place your own executable files into the `bin` folder to be injected into your PATH. 1. Run cmder *(There will be a version with installer)* @@ -60,6 +60,12 @@ For example there is one defined for you `alias e.=explorer .` All aliases will be saved in `/config/aliases` file +### SSH Agent + +To start SSH agent simply call `agent`, which is in the `bin` folder. + +If you want to run SSH agent on startup, uncomment the line in `/vendor/init.bat`so it says `@call "%CMDER_ROOT%/bin/agent.cmd"`. + ## Todo 1. Complete PowerShell compatibility. @@ -74,7 +80,7 @@ All software included is bundled with own license The MIT License (MIT) -Copyright (c) 2013 Samuel Vasko +Copyright (c) 2015 Samuel Vasko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bin/agent.cmd b/bin/agent.cmd new file mode 100644 index 0000000..4eaa614 --- /dev/null +++ b/bin/agent.cmd @@ -0,0 +1,46 @@ +@ECHO OFF + +REM Set default sock file +SET SSH_AUTH_SOCK=/tmp/ssh-agent.sock + +REM Check socket is available +IF NOT EXIST "%TMP%\ssh-agent.sock" GOTO:RUNAGENT + +REM Check if an ssh-agent is running +FOR /f "tokens=*" %%I IN ('ps ^| grep ssh-agent ^| sed "s/^ *\([0-9]\+\) .*/\1/"') DO SET VAR=%%I +IF "%VAR%" == "" GOTO:RUNAGENT + +REM Check if socket file is valid +ssh-add -l 1> NUL 2>&1 +IF ERRORLEVEL 1 GOTO:RUNAGENT +GOTO:ADDKEYS + +:RUNAGENT +REM Remove old socket file +rm -f /tmp/ssh-agent.sock + +REM Run ssh-agent and save (last) PID in VAR +SET VAR= +FOR /f "tokens=*" %%J IN ('ssh-agent -a /tmp/ssh-agent.sock') DO FOR /f "tokens=*" %%K IN ('echo %%J ^| grep "SSH_AGENT_PID" ^| sed "s/^SSH_AGENT_PID=\([0-9]\+\); .*/\1/"') DO SET VAR=%%K + +:ADDKEYS +SET SSH_AUTH_PID=%VAR% + +REM Check if ssh keys are known +SET KEYS= +FOR /f "tokens=*" %%I IN ('DIR /B "%HOME%\.ssh\*_rsa"') DO CALL:CHECKKEY %%I + +REM Add missing ssh keys at once +IF NOT "%KEYS%" == "" ssh-add %KEYS% +GOTO:END + +REM Functions +REM Check if ssh key has to be added +:CHECKKEY +SET VAR= +FOR /f "tokens=*" %%J IN ('ssh-add -l ^| grep "%1"') DO SET VAR=%%J +IF "%VAR%" == "" SET KEYS='%HOME%\.ssh\%1' %KEYS% +GOTO:EOF + +:END +@ECHO ON diff --git a/bin/alias.bat b/bin/alias.bat index 4ec1882..3785621 100644 --- a/bin/alias.bat +++ b/bin/alias.bat @@ -1,6 +1,18 @@ @echo off + +set ALIASES=%CMDER_ROOT%\config\aliases + +if ["%*"] == [""] echo Use /? for help & echo. & goto :p_show if ["%1"] == ["/?"] goto:p_help -if ["%2"] == [""] echo Insufficient parameters. & goto:p_help +if ["%1"] == ["/reload"] goto:p_reload +:: /d flag for delete existing alias +if ["%1"] == ["/d"] goto:p_del %* +:: if arg is an existing alias, display it +if ["%2"] == [""] ( + doskey /macros | findstr /b %1= && goto:eof + echo Insufficient parameters. & goto:p_help +) + ::validate alias setlocal for /f "delims== tokens=1" %%G in ("%*") do set _temp2=%%G @@ -13,15 +25,36 @@ if not ["%_temp%"] == ["%_temp2%"] ( goto:eof ) -echo %* >> "%CMDER_ROOT%\config\aliases" -doskey /macrofile="%CMDER_ROOT%\config\aliases" -echo Alias created +:: replace already defined alias +findstr /b /v /i "%_temp%=" "%ALIASES%" >> "%ALIASES%.tmp" +echo %* >> "%ALIASES%.tmp" && type "%ALIASES%.tmp" > "%ALIASES%" & @del /f /q "%ALIASES%.tmp" +doskey /macrofile="%ALIASES%" endlocal goto:eof +:p_del +findstr /b /v /i "%2=" "%ALIASES%" >> "%ALIASES%.tmp" +type "%ALIASES%".tmp > "%ALIASES%" & @del /f /q "%ALIASES%.tmp" +doskey /macrofile=%ALIASES% +goto:eof + +:p_reload +doskey /macrofile="%ALIASES%" +echo Aliases reloaded +goto:eof + +:p_show +type "%ALIASES%" || echo No aliases found at "%ALIASES%" +goto :eof + :p_help echo.Usage: -echo. alias name=full command +echo. alias [/reload] [/d] [name=full command] +echo. /reload Reload the aliases file +echo. /d Delete an alias (must be followed by the alias name) +echo. +echo. If alias is called with any parameters, it will display the list of existing aliases. +echo. In the command, you can use the following notations: echo. $* allows the alias to assume all the parameters of the supplied command. echo. $1-$9 Allows you to seperate parameter by number, much like %%1 in batch. echo. $T is the command seperator, allowing you to string several commands together into one alias. diff --git a/config/ConEmu.xml b/config/ConEmu.xml index 19fd4c5..7ffe3ba 100644 --- a/config/ConEmu.xml +++ b/config/ConEmu.xml @@ -1,7 +1,7 @@ - + @@ -72,7 +72,7 @@ - + @@ -127,8 +127,8 @@ - - + + @@ -138,7 +138,7 @@ - + @@ -349,7 +349,7 @@ - + @@ -419,8 +419,8 @@ - - + + @@ -483,30 +483,51 @@ - - - + + + - + + - + - - + + + + + + + + + + + + + + + + + + + + + - + + - - + + @@ -581,6 +602,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/aliases b/config/aliases index b2fdae1..41d71ad 100644 --- a/config/aliases +++ b/config/aliases @@ -1,5 +1,7 @@ e.=explorer . gl=git log --oneline --all --graph --decorate $* -ls=ls --color $* +ls=ls --show-control-chars -F --color $* pwd=cd clear=cls +history=cat %CMDER_ROOT%\config\.history +unalias=alias /d $1 diff --git a/config/cmder.lua b/config/cmder.lua new file mode 100644 index 0000000..b63fe95 --- /dev/null +++ b/config/cmder.lua @@ -0,0 +1,184 @@ +function lambda_prompt_filter() + clink.prompt.value = string.gsub(clink.prompt.value, "{lamb}", "λ") +end + +--- + -- Resolves closest directory location for specified directory. + -- Navigates subsequently up one level and tries to find specified directory + -- @param {string} path Path to directory will be checked. If not provided + -- current directory will be used + -- @param {string} dirname Directory name to search for + -- @return {string} Path to specified directory or nil if such dir not found +local function get_dir_contains(path, dirname) + + -- return parent path for specified entry (either file or directory) + local function pathname(path) + local prefix = "" + local i = path:find("[\\/:][^\\/:]*$") + if i then + prefix = path:sub(1, i-1) + end + return prefix + end + + -- Navigates up one level + local function up_one_level(path) + if path == nil then path = '.' end + if path == '.' then path = clink.get_cwd() end + return pathname(path) + end + + -- Checks if provided directory contains git directory + local function has_specified_dir(path, specified_dir) + if path == nil then path = '.' end + local found_dirs = clink.find_dirs(path..'/'..specified_dir) + if #found_dirs > 0 then return true end + return false + end + + -- Set default path to current directory + if path == nil then path = '.' end + + -- If we're already have .git directory here, then return current path + if has_specified_dir(path, dirname) then + return path..'/'..dirname + else + -- Otherwise go up one level and make a recursive call + local parent_path = up_one_level(path) + if parent_path == path then + return nil + else + return get_dir_contains(parent_path, dirname) + end + end +end + +local function get_hg_dir(path) + return get_dir_contains(path, '.hg') +end + +local function get_git_dir(path) + return get_dir_contains(path, '.git') +end + +--- + -- Find out current branch + -- @return {false|mercurial branch name} +--- +function get_hg_branch() + for line in io.popen("hg branch 2>nul"):lines() do + local m = line:match("(.+)$") + if m then + return m + end + end + + return false +end + +--- + -- Get the status of working dir + -- @return {bool} +--- +function get_hg_status() + for line in io.popen("hg status"):lines() do + return false + end + return true +end + +function hg_prompt_filter() + + -- Colors for mercurial status + local colors = { + clean = "\x1b[1;37;40m", + dirty = "\x1b[31;1m", + } + + if get_hg_dir() then + -- if we're inside of mercurial repo then try to detect current branch + local branch = get_hg_branch() + if branch then + -- Has branch => therefore it is a mercurial folder, now figure out status + if get_hg_status() then + color = colors.clean + else + color = colors.dirty + end + + clink.prompt.value = string.gsub(clink.prompt.value, "{hg}", color.."("..branch..")") + return false + end + end + + -- No mercurial present or not in mercurial file + clink.prompt.value = string.gsub(clink.prompt.value, "{hg}", "") + return false +end + +--- + -- Find out current branch + -- @return {false|git branch name} +--- +function get_git_branch() + for line in io.popen("git branch 2>nul"):lines() do + local m = line:match("%* (.+)$") + if m then + return m + end + end + + return false +end + +--- + -- Get the status of working dir + -- @return {bool} +--- +function get_git_status() + return os.execute("git diff --quiet --ignore-submodules HEAD 2>nul") +end + +function git_prompt_filter() + + -- Colors for git status + local colors = { + clean = "\x1b[1;37;40m", + dirty = "\x1b[31;1m", + } + + if get_git_dir() then + -- if we're inside of git repo then try to detect current branch + local branch = get_git_branch() + if branch then + -- Has branch => therefore it is a git folder, now figure out status + if get_git_status() then + color = colors.clean + else + color = colors.dirty + end + + clink.prompt.value = string.gsub(clink.prompt.value, "{git}", color.."("..branch..")") + return false + end + end + + -- No git present or not in git file + clink.prompt.value = string.gsub(clink.prompt.value, "{git}", "") + return false +end + +clink.prompt.register_filter(lambda_prompt_filter, 40) +clink.prompt.register_filter(hg_prompt_filter, 50) +clink.prompt.register_filter(git_prompt_filter, 50) + +local completions_dir = clink.get_env('CMDER_ROOT')..'/vendor/clink-completions/' +for _,lua_module in ipairs(clink.find_files(completions_dir..'*.lua')) do + -- Skip files that starts with _. This could be useful if some files should be ignored + if not string.match(lua_module, '^_.*') then + local filename = completions_dir..lua_module + -- use dofile instead of require because require caches loaded modules + -- so config reloading using Alt-Q won't reload updated modules. + dofile(filename) + end +end \ No newline at end of file diff --git a/config/git.lua b/config/git.lua deleted file mode 100644 index 94b9536..0000000 --- a/config/git.lua +++ /dev/null @@ -1,50 +0,0 @@ ---- - -- Find out current branch - -- @return {false|git branch name} ---- -function get_git_branch() - for line in io.popen("git branch 2>nul"):lines() do - local m = line:match("%* (.+)$") - if m then - return m - end - end - - return false -end - ---- - -- Get the status of working dir - -- @return {bool} ---- -function get_git_status() - return os.execute("git diff --quiet --ignore-submodules HEAD") -end - -function git_prompt_filter() - - -- Colors for git status - local colors = { - clean = "\x1b[1;37;40m", - dirty = "\x1b[31;1m", - } - - local branch = get_git_branch() - if branch then - -- Has branch => therefore it is a git folder, now figure out status - if get_git_status() then - color = colors.clean - else - color = colors.dirty - end - - clink.prompt.value = string.gsub(clink.prompt.value, "{git}", color.."("..branch..")") - return true - end - - -- No git present or not in git file - clink.prompt.value = string.gsub(clink.prompt.value, "{git}", "") - return false -end - -clink.prompt.register_filter(git_prompt_filter, 50) \ No newline at end of file diff --git a/config/prompt.lua b/config/prompt.lua deleted file mode 100644 index 5b0c239..0000000 --- a/config/prompt.lua +++ /dev/null @@ -1,5 +0,0 @@ -function lambda_prompt_filter() - clink.prompt.value = string.gsub(clink.prompt.value, "{lamb}", "λ") -end - -clink.prompt.register_filter(lambda_prompt_filter, 40) \ No newline at end of file diff --git a/config/settings b/config/settings index 93de0be..ea1bd7e 100644 --- a/config/settings +++ b/config/settings @@ -27,7 +27,7 @@ match_colour = -1 # the line. 0 = PATH only, 1 = PATH and CWD, 2 = PATH, CWD, and directories. In # all cases both executables and directories are matched when there is a path # separator present. -exec_match_style = -1 +exec_match_style = 2 # name: Prompt colour # type: int diff --git a/launcher/CmderLauncher.sln b/launcher/CmderLauncher.sln index e130cc2..74e5bdd 100644 --- a/launcher/CmderLauncher.sln +++ b/launcher/CmderLauncher.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Express 2013 for Windows Desktop -VisualStudioVersion = 12.0.21005.1 +# Visual Studio 14 +VisualStudioVersion = 14.0.22823.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmderLauncher", "CmderLauncher.vcxproj", "{4A8485A5-B7DD-4C44-B7F6-3E2765DD0CD3}" EndProject diff --git a/launcher/CmderLauncher.vcxproj b/launcher/CmderLauncher.vcxproj index f4ab5c3..d9390f5 100644 --- a/launcher/CmderLauncher.vcxproj +++ b/launcher/CmderLauncher.vcxproj @@ -1,5 +1,5 @@  - + Debug @@ -14,18 +14,19 @@ {4A8485A5-B7DD-4C44-B7F6-3E2765DD0CD3} Win32Proj CmderLauncher + 8.1 Application true - v120 + v140_xp Unicode Application false - v120 + v140_xp true Unicode diff --git a/launcher/src/CmderLauncher.cpp b/launcher/src/CmderLauncher.cpp index fcdeb60..893e33c 100644 --- a/launcher/src/CmderLauncher.cpp +++ b/launcher/src/CmderLauncher.cpp @@ -79,7 +79,7 @@ optpair GetOption() return pair; } -void StartCmder(std::wstring path) +void StartCmder(std::wstring path, bool is_single_mode) { #if USE_TASKBAR_API wchar_t appId[MAX_PATH] = { 0 }; @@ -102,7 +102,14 @@ void StartCmder(std::wstring path) PathCombine(cfgPath, exeDir, L"config\\ConEmu.xml"); PathCombine(conEmuPath, exeDir, L"vendor\\conemu-maximus5\\ConEmu.exe"); - swprintf_s(args, L"/Icon \"%s\" /Title Cmder /LoadCfgFile \"%s\"", icoPath, cfgPath); + if (is_single_mode) + { + swprintf_s(args, L"/single /Icon \"%s\" /Title Cmder /LoadCfgFile \"%s\"", icoPath, cfgPath); + } + else + { + swprintf_s(args, L"/Icon \"%s\" /Title Cmder /LoadCfgFile \"%s\"", icoPath, cfgPath); + } SetEnvironmentVariable(L"CMDER_ROOT", exeDir); SetEnvironmentVariable(L"CMDER_START", path.c_str()); @@ -229,7 +236,11 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, if (streqi(opt.first.c_str(), L"/START")) { - StartCmder(opt.second); + StartCmder(opt.second, false); + } + else if (streqi(opt.first.c_str(), L"/SINGLE")) + { + StartCmder(opt.second, true); } else if (streqi(opt.first.c_str(), L"/REGISTER")) { @@ -243,7 +254,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, } else { - MessageBox(NULL, L"Unrecognized parameter.\n\nValid options:\n /START \n /REGISTER [USER/ALL]\n /UNREGISTER [USER/ALL]", MB_TITLE, MB_OK); + MessageBox(NULL, L"Unrecognized parameter.\n\nValid options:\n /START \n /SINGLE \n /REGISTER [USER/ALL]\n /UNREGISTER [USER/ALL]", MB_TITLE, MB_OK); return 1; } diff --git a/msvcp120.dll b/msvcp120.dll deleted file mode 100644 index a237d2d..0000000 Binary files a/msvcp120.dll and /dev/null differ diff --git a/msvcr120.dll b/msvcr120.dll deleted file mode 100644 index 8c36149..0000000 Binary files a/msvcr120.dll and /dev/null differ diff --git a/packignore b/packignore index 12d1d29..b459393 100644 --- a/packignore +++ b/packignore @@ -1,5 +1,4 @@ launcher -icons .gitignore .gitattributes .git @@ -7,4 +6,4 @@ icons build config\.history packignore -Cmder.bat \ No newline at end of file +Cmder.bat diff --git a/scripts/build.ps1 b/scripts/build.ps1 index f4a908e..cca62df 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -11,7 +11,11 @@ .EXAMPLE .\build.ps1 - Executes the default build for cmder, this is equivalent to the "minimum" style package in the releases + Executes the default build for cmder; Conemu, clink. This is equivalent to the "minimum" style package in the releases +.EXAMPLE + .\build.ps1 -Full + + Executes a full build for cmder, including git. This is equivalent to the "full" style package in the releases .EXAMPLE .\build -verbose @@ -27,7 +31,6 @@ .LINK https://github.com/bliker/cmder - Project Home #> - [CmdletBinding(SupportsShouldProcess=$true)] Param( # CmdletBinding will give us; @@ -41,7 +44,10 @@ Param( [string]$saveTo = "..\vendor\", # Launcher folder location - [string]$launcher = "..\launcher" + [string]$launcher = "..\launcher", + + # Include git with the package build + [switch]$Full ) . "$PSScriptRoot\utils.ps1" @@ -53,12 +59,17 @@ $sources = Get-Content $sourcesPath | Out-String | Convertfrom-Json # Check for requirements Ensure-Exists $sourcesPath Ensure-Executable "7z" +New-Item -Type Directory -Path (Join-Path $saveTo "/tmp/") -ErrorAction SilentlyContinue >$null foreach ($s in $sources) { + if($Full -eq $false -and $s.name -eq "msysgit"){ + Continue + } + Write-Verbose "Getting $($s.name) from URL $($s.url)" # We do not care about the extensions/type of archive - $tempArchive = "$($s.name).tmp" + $tempArchive = "tmp/$($s.name).tmp" Delete-Existing $tempArchive Delete-Existing $s.name @@ -68,6 +79,8 @@ foreach ($s in $sources) { if ((Get-Childitem $s.name).Count -eq 1) { Flatten-Directory($s.name) } + # Write current version to .cmderver file, for later. + "$($s.version)" | Out-File "$($s.name)/.cmderver" } Pop-Location diff --git a/scripts/pack.ps1 b/scripts/pack.ps1 index 78e8af7..178d3a8 100644 --- a/scripts/pack.ps1 +++ b/scripts/pack.ps1 @@ -16,7 +16,7 @@ Creates default archives for cmder with plenty of information .NOTES AUTHORS - Samuel Vasko, Jack Bennett + Samuel Vasko, Jack Bennett, Martin Kemp Part of the Cmder project. .LINK https://github.com/bliker/cmder - Project Home @@ -46,6 +46,7 @@ $targets = @{ } Delete-Existing "..\Version*" +Cleanup-Git $version = Invoke-Expression "git describe --abbrev=0 --tags" (New-Item -ItemType file "$cmderRoot\Version $version") | Out-Null diff --git a/scripts/utils.ps1 b/scripts/utils.ps1 index 714c983..8657bf6 100644 --- a/scripts/utils.ps1 +++ b/scripts/utils.ps1 @@ -13,7 +13,7 @@ function Ensure-Executable ($command) { set-alias -Name "7z" -Value "$env:programfiles\7-zip\7z.exe" -Scope script } ElseIf( ($command -eq "7z") -and (Test-Path "$env:programw6432\7-zip\7z.exe") ) { - set-alias -Name "7z" -Value "$env:programw6432\7-zip\7z.exe" -Scope script + set-alias -Name "7z" -Value "$env:programw6432\7-zip\7z.exe" -Scope script } Else { Write-Error "Missing $command! Ensure it is installed and on in the PATH" @@ -28,7 +28,7 @@ function Delete-Existing ($path) { } function Extract-Archive ($source, $target) { - Invoke-Expression "7z x -y -o$($target) $source > `$null" + Invoke-Expression "7z x -y -o$($target) '$source' > `$null" if ($lastexitcode -ne 0) { Write-Error "Extracting of $source failied" } @@ -54,5 +54,43 @@ function Flatten-Directory ($name) { } function Digest-MD5 ($path) { + if(Get-Command Get-FileHash -ErrorAction SilentlyContinue){ + return (Get-FileHash -Algorithm MD5 -Path $path).Hash + } + return Invoke-Expression "md5sum $path" } + +function Cleanup-Git () { + $gitdir = '/vendor/msysgit/libexec/git-core/' + Get-Childitem $gitdir -Exclude git.exe | Where-Object{!($_.PSIsContainer)} | Foreach-Object { Remove-Item $_.FullName } +} + +function Register-Cmder(){ + [CmdletBinding()] + Param + ( + # Text for the context menu item. + $MenuText = "Cmder Here" + + , # Defaults to the current cmder directory when run from cmder. + $PathToExe = (Join-Path $env:CMDER_ROOT "cmder.exe") + + , # Commands the context menu will execute. + $Command = "%V" + + , # Defaults to the icons folder in the cmder package. + $icon = (Split-Path $PathToExe | join-path -ChildPath 'icons/cmder.ico') + ) + Begin + { + New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT + } + Process + { + New-Item -Path "HKCR:\Directory\Shell\Cmder" -Force -Value $MenuText + New-ItemProperty -Path "HKCR:\Directory\Shell\Cmder" -Force -Name "Icon" -Value `"$icon`" + New-ItemProperty -Path "HKCR:\Directory\Shell\Cmder" -Force -Name "NoWorkingDirectory" + New-Item -Path "HKCR:\Directory\Shell\Cmder\Command" -Force -Value "`"$PathToExe`" `"$Command`" " + } +} diff --git a/vendor/init.bat b/vendor/init.bat index 308f932..7a7f02c 100644 --- a/vendor/init.bat +++ b/vendor/init.bat @@ -4,12 +4,12 @@ :: Find root dir @if not defined CMDER_ROOT ( - for /f %%i in ("%ConEmuDir%\..\..") do @set CMDER_ROOT=%%~fi + for /f "delims=" %%i in ("%ConEmuDir%\..\..") do @set CMDER_ROOT=%%~fi ) :: Change the prompt style :: Mmm tasty lamb -@prompt $E[1;32;40m$P$S{git}$S$_$E[1;30;40m{lamb}$S$E[0m +@prompt $E[1;32;40m$P$S{git}{hg}$S$_$E[1;30;40m{lamb}$S$E[0m :: Pick right version of clink @if "%PROCESSOR_ARCHITECTURE%"=="x86" ( @@ -27,9 +27,24 @@ @set PLINK_PROTOCOL=ssh @if not defined TERM set TERM=cygwin +:: Check if msysgit is installed +@if exist "%ProgramFiles%\Git" ( + set "GIT_INSTALL_ROOT=%ProgramFiles%\Git" +) else if exist "%ProgramFiles(x86)%\Git" ( + set "GIT_INSTALL_ROOT=%ProgramFiles(x86)%\Git" +) else if exist "%CMDER_ROOT%\vendor" ( + set "GIT_INSTALL_ROOT=%CMDER_ROOT%\vendor\msysgit" +) + +:: Add git to the path +@if defined GIT_INSTALL_ROOT ( + set "PATH=%GIT_INSTALL_ROOT%\bin;%GIT_INSTALL_ROOT%\share\vim\vim74;%PATH%" + :: define SVN_SSH so we can use git svn with ssh svn repositories + if not defined SVN_SSH set "SVN_SSH=%GIT_INSTALL_ROOT:\=\\%\\bin\\ssh.exe" +) + :: Enhance Path -@set git_install_root=%CMDER_ROOT%\vendor\msysgit -@set PATH=%CMDER_ROOT%\bin;%git_install_root%\bin;%git_install_root%\mingw\bin;%git_install_root%\cmd;%git_install_root%\share\vim\vim74;%CMDER_ROOT%;%PATH% +@set PATH=%CMDER_ROOT%\bin;%PATH%;%CMDER_ROOT% :: Add aliases @doskey /macrofile="%CMDER_ROOT%\config\aliases" @@ -44,3 +59,5 @@ @cd /d "%HOME%" ) ) + +:: @call "%CMDER_ROOT%/bin/agent.cmd" diff --git a/vendor/profile.ps1 b/vendor/profile.ps1 new file mode 100644 index 0000000..fcaa56e --- /dev/null +++ b/vendor/profile.ps1 @@ -0,0 +1,52 @@ +# Add Cmder modules directory to the autoload path. +$CmderModulePath = Join-path $PSScriptRoot "psmodules/" + +if( -not $env:PSModulePath.Contains($CmderModulePath) ){ + $env:PSModulePath = $env:PSModulePath.Insert(0, "$CmderModulePath;") +} + +try { + Get-command -Name "git" -ErrorAction Stop >$null + Import-Module -Name "posh-git" -ErrorAction Stop >$null + $gitStatus = $true +} catch { + Write-Warning "Missing git support, install posh-git with 'Install-Module posh-git' and restart cmder." + $gitStatus = $false +} + +function checkGit($Path) { + if (Test-Path -Path (Join-Path $Path '.git/') ) { + Write-VcsStatus + return + } + $SplitPath = split-path $path + if ($SplitPath) { + checkGit($SplitPath) + } +} + +# 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($gitStatus){ + checkGit($pwd.ProviderPath) + } + $global:LASTEXITCODE = $realLASTEXITCODE + Write-Host "`nλ" -NoNewLine -ForegroundColor "DarkGray" + return " " +} + +# Load special features come from posh-git +if ($gitStatus) { + 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.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