diff --git a/PSDepend/PSDependScripts/PSGalleryModule.ps1 b/PSDepend/PSDependScripts/PSGalleryModule.ps1 index 53b1661..1571cc3 100644 --- a/PSDepend/PSDependScripts/PSGalleryModule.ps1 +++ b/PSDepend/PSDependScripts/PSGalleryModule.ps1 @@ -35,6 +35,9 @@ Install: Install the dependency Import: Import the dependency + .PARAMETER Clean + Deletes existing versions of the module before installing/saving desired version + .EXAMPLE @{ BuildHelpers = 'latest' @@ -86,44 +89,52 @@ param( [switch]$Import, [ValidateSet('Test', 'Install', 'Import')] - [string[]]$PSDependAction = @('Install') + [string[]]$PSDependAction = @('Install'), + + [switch]$Clean ) # Extract data from Dependency - $DependencyName = $Dependency.DependencyName - $Name = $Dependency.Name - if(-not $Name) - { - $Name = $DependencyName - } - - $Version = $Dependency.Version - if(-not $Version) - { - $Version = 'latest' - } +$Name = $Dependency.Name +if(-not $Name) +{ + $Name = $Dependency.DependencyName +} - # We use target as a proxy for Scope - if(-not $Dependency.Target) - { - $Scope = 'AllUsers' - } - else - { - $Scope = $Dependency.Target - } +$findModuleSplat = @{ + Name = $Name +} - $Credential = $Dependency.Credential +$moduleSplat = @{ + Name = $Name + Verbose = $VerbosePreference + Force = $True +} - if('AllUsers', 'CurrentUser' -notcontains $Scope) +# We use target as a proxy for Scope +$install = $True +if($Dependency.Target -and 'AllUsers', 'CurrentUser' -notcontains $Dependency.Target) +{ + $install = $false + $moduleFullname = Join-Path -Path $Dependency.Target -ChildPath $Name +} +else +{ + if ($Dependency.Target) { - $command = 'save' + $scope = $Dependency.Target } else { - $command = 'install' + $scope = 'AllUsers' } + $moduleFullname = $Name + $moduleSplat['AllowClobber'] = $AllowClobber + $moduleSplat['SkipPublisherCheck'] = $SkipPublisherCheck + $moduleSplat['Scope'] = $scope +} + if(-not (Get-PackageProvider -Name Nuget)) { # Grab nuget bits. @@ -134,140 +145,130 @@ Write-Verbose -Message "Getting dependency [$name] from PowerShell repository [$ # Validate that $target has been setup as a valid PowerShell repository, # but allow to rely on all PS repos registered. -if($Repository) { - $validRepo = Get-PSRepository -Name $Repository -Verbose:$false -ErrorAction SilentlyContinue - if (-not $validRepo) { - Write-Error "[$Repository] has not been setup as a valid PowerShell repository." - return - } -} - -$params = @{ - Name = $Name - SkipPublisherCheck = $SkipPublisherCheck - AllowClobber = $AllowClobber - Verbose = $VerbosePreference - Force = $True -} - -if($Repository) { - $params.Add('Repository',$Repository) +if($Repository) +{ + if (-not (Get-PSRepository -Name $Repository -Verbose:$false -ErrorAction SilentlyContinue)) + { + Write-Error "[$Repository] has not been setup as a valid PowerShell repository." + return + } } -if( $Version -and $Version -ne 'latest') +if ($Repository) { - $Params.add('RequiredVersion',$Version) + $findModuleSplat.Add('Repository',$Repository) + $moduleSplat.Add('Repository',$Repository) } -if($Credential) +if ($Dependency.Credential) { - $Params.add('Credential', $Credential) + $findModuleSplat.Add('Credential', $Dependency.Credential) + $moduleSplat.add('Credential', $Dependency.Credential) } -# This code works for both install and save scenarios. -if($command -eq 'Save') +if ($Dependency.Version -and $Dependency.Version -ne 'latest') { - $ModuleName = Join-Path $Scope $Name - $Params.Remove('AllowClobber') - $Params.Remove('SkipPublisherCheck') + $Version = $Dependency.Version + $moduleSplat.add('RequiredVersion',$Version) } -elseif ($Command -eq 'Install') +else { - $ModuleName = $Name + $Version = (Find-Module @findModuleSplat).Version.ToString() } +Write-Verbose "Targetting module '$Name' Version: $Version" + # Only use "SkipPublisherCheck" (and other) parameter if "Install-Module" supports it $availableParameters = (Get-Command "Install-Module").Parameters -$tempParams = $Params.Clone() -foreach($thisParameter in $Params.Keys) +$tempmoduleSplat = $moduleSplat.Clone() +foreach($thisParameter in $moduleSplat.Keys) { if(-Not ($availableParameters.ContainsKey($thisParameter))) { Write-Verbose -Message "Removing parameter [$thisParameter] from [Install-Module] as it is not available" - $tempParams.Remove($thisParameter) + $tempmoduleSplat.Remove($thisParameter) } } -$Params = $tempParams.Clone() +$moduleSplat = $tempmoduleSplat.Clone() Add-ToPsModulePathIfRequired -Dependency $Dependency -Action $PSDependAction -$Existing = $null -$Existing = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue - -if($Existing) +$existingModules = $null +$existingModules = Get-Module -ListAvailable -Name $Name -ErrorAction SilentlyContinue + +if ($PSDependAction -contains 'Install') { - Write-Verbose "Found existing module [$Name]" - # Thanks to Brandon Padgett! - $ExistingVersion = $Existing | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum - $FindModuleParams = @{Name = $Name} - if($Repository) { - $FindModuleParams.Add('Repository',$Repository) - } - if($Credential) - { - $FindModuleParams.Add('Credential', $Credential) - } - - $GetGalleryVersion = { Find-Module @FindModuleParams | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum } - - # Version string, and equal to current - if( $Version -and $Version -ne 'latest' -and $Version -eq $ExistingVersion) + # If Clean is set to $true, cleanup any existing versions of the module + if ($PSDependAction -notcontains 'Test' -and $Clean -and $existingModules) { - Write-Verbose "You have the requested version [$Version] of [$Name]" - # Conditional import - Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion - - if($PSDependAction -contains 'Test') + Write-Verbose "Parameter 'Clean' set to 'true', removing existing versions..." + foreach ($existingModule in $existingModules) { - return $True + $existingVersion = $existingModule.Version.ToString() + Write-Verbose "Found existing module: '$Name' Version: $existingVersion" + if ($existingModule.ModuleBase) + { + $differentModulePath = (Get-Item -Path $existingModule.ModuleBase).Parent.FullName -ne $moduleFullname + } + + if (($existingVersion -ne $Version -and $Dependency.Target -and $differentModulePath) -or $existingVersion -ne $Version) + { + # Remove module from session just in case + Write-Verbose "Removing existing module: '$($existingModule.Name)' Version: $existingVersion" + Remove-Module -Name $existingModule.Name -Force -ErrorAction SilentlyContinue -Verbose:$false + + # Pause to give the module a chance to be fully removed from session + Start-Sleep -Seconds 1 + Remove-Item -Path $existingModule.ModuleBase -Force -Recurse + } } - return $null } - - # latest, and we have latest - if( $Version -and - ($Version -eq 'latest' -or $Version -like '') -and - [System.Version]($GalleryVersion = (& $GetGalleryVersion)) -le [System.Version]$ExistingVersion - ) + + if (($existingModules -and ($existingModules | Foreach-Object {$_.Version.ToString()}) -notcontains $Version) -or -not $existingModules) { - Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and PSGallery version [$GalleryVersion]" - # Conditional import - Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion + if (-not $install) + { + if (-not (Test-Path -Path $moduleFullname -ErrorAction SilentlyContinue)) + { + Write-Verbose "Creating directory path to '$moduleFullname'" + $null = New-Item -ItemType Directory -Path $moduleFullname -Force -ErrorAction SilentlyContinue + } - if($PSDependAction -contains 'Test') + $modulePath = Join-Path -Path $moduleFullname -ChildPath $Version + if(-not (Test-Path -Path $modulePath -ErrorAction SilentlyContinue)) + { + Write-Verbose "Saving '$Name' with path '$moduleFullname'" + Save-Module @moduleSplat -Path (Split-Path -Path $moduleFullname -Parent) + } + } + else { - return $True + Write-Verbose "Installing [$Name] with scope [$Scope]" + Install-Module @moduleSplat } - return $null } - Write-Verbose "Continuing to install [$Name]: Requested version [$version], existing version [$ExistingVersion]" } -#No dependency found, return false if we're testing alone... -if( $PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) +if ($PSDependAction -contains 'Import') { - return $False + Write-Verbose "You have the requested version [$Version] of [$Name]" + # Conditional import + Import-PSDependModule -Name $moduleFullname -Action $PSDependAction -Version $Version + + if($PSDependAction -contains 'Test') + { + return $True + } } -if($PSDependAction -contains 'Install') +if ($PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) { - if('AllUsers', 'CurrentUser' -contains $Scope) + if ($existingModules) { - Write-Verbose "Installing [$Name] with scope [$Scope]" - Install-Module @params -Scope $Scope + return ($existingModules | Foreach-Object {$_.Version.ToString()}) -contains $Version } else { - Write-Verbose "Saving [$Name] with path [$Scope]" - Write-Verbose "Creating directory path to [$Scope]" - if(-not (Test-Path $Scope -ErrorAction SilentlyContinue)) - { - $Null = New-Item -ItemType Directory -Path $Scope -Force -ErrorAction SilentlyContinue - } - Save-Module @params -Path $Scope + return $false } } - -# Conditional import -$importVs = $params['RequiredVersion'] -Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $importVs diff --git a/Tests/DependFiles/psgallerymodule.clean.depend.psd1 b/Tests/DependFiles/psgallerymodule.clean.depend.psd1 new file mode 100644 index 0000000..050a405 --- /dev/null +++ b/Tests/DependFiles/psgallerymodule.clean.depend.psd1 @@ -0,0 +1,7 @@ +@{ + 'Pester' = @{ + Version = '4.1.0' + Target = 'TestDrive:/PSDependPesterTest' + Parameters = @{Clean = $true} + } +} \ No newline at end of file diff --git a/Tests/DependFiles/psgallerymodule.cleannotarget.depend.psd1 b/Tests/DependFiles/psgallerymodule.cleannotarget.depend.psd1 new file mode 100644 index 0000000..5bf9165 --- /dev/null +++ b/Tests/DependFiles/psgallerymodule.cleannotarget.depend.psd1 @@ -0,0 +1,6 @@ +@{ + 'Pester' = @{ + Version = '4.1.0' + Parameters = @{Clean = $true} + } +} \ No newline at end of file diff --git a/Tests/PSModuleGallery.Type.Tests.ps1 b/Tests/PSModuleGallery.Type.Tests.ps1 index 5a9a69c..1b25281 100644 --- a/Tests/PSModuleGallery.Type.Tests.ps1 +++ b/Tests/PSModuleGallery.Type.Tests.ps1 @@ -33,7 +33,7 @@ InModuleScope 'PSDepend' { Describe "PSGalleryModule Type PS$PSVersion" { $SavePath = (New-Item 'TestDrive:/PSDependPesterTest' -ItemType Directory -Force).FullName - + Context 'Installs Modules' { Mock Install-Module { Return $true } @@ -50,6 +50,7 @@ InModuleScope 'PSDepend' { Context 'Installs Modules with credentials' { Mock Install-Module { Return $true } + Mock Find-Module { return @{Version = '1.0'} } $Results = Invoke-PSDepend @Verbose -Path "$TestDepends/psgallerymodule.withcredentials.depend.psd1" -Force -Credentials $Credentials @@ -64,6 +65,7 @@ InModuleScope 'PSDepend' { Context 'Installs Modules with multiple credentials' { Mock Install-Module { Return $true } + Mock Find-Module { return @{Version = '1.0'} } $Results = Invoke-PSDepend @Verbose -Path "$TestDepends/psgallerymodule.multiplecredentials.depend.psd1" -Force -Credentials $Credentials @@ -303,8 +305,80 @@ InModuleScope 'PSDepend' { } } } - } + Context 'Clean' { + + Mock Remove-Module {} + Mock Remove-Item {} + Mock Save-Module {} + + It 'Deletes older version of module' { + Mock Get-Item {return $true} + Mock Get-Module { + [pscustomobject]@{ + Name = 'Pester' + Version = '4.9.0' + ModuleBase = 'TestDrive:\PSDependPesterTest' + } + } + + Invoke-PSDepend @Verbose -Path "$TestDepends\psgallerymodule.clean.depend.psd1" -Force -ErrorAction Stop + Assert-MockCalled -CommandName Remove-Module -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Remove-Item -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Save-Module -Times 1 -Exactly -Scope It + } + + It 'Deletes older version of module without target' { + Mock Get-Item {return $true} + Mock Install-Module {} + Mock Get-Module { + [pscustomobject]@{ + Name = 'Pester' + Version = '4.9.0' + ModuleBase = 'TestDrive:\PSDependPesterTest' + } + } + + Invoke-PSDepend @Verbose -Path "$TestDepends\psgallerymodule.cleannotarget.depend.psd1" -Force -ErrorAction Stop + Assert-MockCalled -CommandName Remove-Module -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Remove-Item -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Install-Module -Times 1 -Exactly -Scope It + } + + It 'Does not delete targetted version of module' { + Mock Get-Item {return $true} + Mock Get-Module { + [pscustomobject]@{ + Name = 'Pester' + Version = '4.1.0' + ModuleBase = 'TestDrive:\PSDependPesterTest' + } + } + + Invoke-PSDepend @Verbose -Path "$TestDepends\psgallerymodule.clean.depend.psd1" -Force -ErrorAction Stop + Assert-MockCalled -CommandName Remove-Module -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Remove-Item -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Save-Module -Times 0 -Exactly -Scope It + } + + It 'Does not delete when Test is used' { + Mock Get-Item {return $true} + Mock Get-Module { + [pscustomobject]@{ + Name = 'Pester' + Version = '4.1.0' + ModuleBase = 'TestDrive:\PSDependPesterTest' + } + } + + Invoke-PSDepend @Verbose -Path "$TestDepends\psgallerymodule.clean.depend.psd1" -Test -Force -ErrorAction Stop + Assert-MockCalled -CommandName Remove-Module -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Remove-Item -Times 0 -Exactly -Scope It + Assert-MockCalled -CommandName Save-Module -Times 0 -Exactly -Scope It + } + } + } + Describe "Git Type PS$PSVersion" -Tag "WindowsOnly" { $SavePath = (New-Item 'TestDrive:/PSDependPesterTest' -ItemType Directory -Force).FullName @@ -703,7 +777,7 @@ InModuleScope 'PSDepend' { $Results | Should be $True } } - #> + #> Context 'PackageSource does not Exist' { Mock Install-Package @@ -752,9 +826,9 @@ InModuleScope 'PSDepend' { at line: 188 in C:\projects\psdepend\psdepend\Public\Invoke-DependencyScript.ps1" See build logs: https://ci.appveyor.com/project/RamblingCookieMonster/psdepend/build/1.0.124 - + #> - + function Install-Package {[cmdletbinding()]param( $Source, $Name, $RequiredVersion, $Force)} function Get-PackageSource { @([pscustomobject]@{Name = 'chocolatey'; ProviderName = 'chocolatey'}) } diff --git a/dotnet-install.ps1 b/dotnet-install.ps1 new file mode 100644 index 0000000..872baef --- /dev/null +++ b/dotnet-install.ps1 @@ -0,0 +1,677 @@ +# +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +<# +.SYNOPSIS + Installs dotnet cli +.DESCRIPTION + Installs dotnet cli. If dotnet installation already exists in the given directory + it will update it only if the requested version differs from the one already installed. +.PARAMETER Channel + Default: LTS + Download from the Channel specified. Possible values: + - Current - most current release + - LTS - most current supported release + - 2-part version in a format A.B - represents a specific release + examples: 2.0, 1.0 + - Branch name + examples: release/2.0.0, Master + Note: The version parameter overrides the channel parameter. +.PARAMETER Version + Default: latest + Represents a build version on specific channel. Possible values: + - latest - most latest build on specific channel + - coherent - most latest coherent build on specific channel + coherent applies only to SDK downloads + - 3-part version in a format A.B.C - represents specific version of build + examples: 2.0.0-preview2-006120, 1.1.0 +.PARAMETER InstallDir + Default: %LocalAppData%\Microsoft\dotnet + Path to where to install dotnet. Note that binaries will be placed directly in a given directory. +.PARAMETER Architecture + Default: - this value represents currently running OS architecture + Architecture of dotnet binaries to be installed. + Possible values are: , amd64, x64, x86, arm64, arm +.PARAMETER SharedRuntime + This parameter is obsolete and may be removed in a future version of this script. + The recommended alternative is '-Runtime dotnet'. + Installs just the shared runtime bits, not the entire SDK. +.PARAMETER Runtime + Installs just a shared runtime, not the entire SDK. + Possible values: + - dotnet - the Microsoft.NETCore.App shared runtime + - aspnetcore - the Microsoft.AspNetCore.App shared runtime + - windowsdesktop - the Microsoft.WindowsDesktop.App shared runtime +.PARAMETER DryRun + If set it will not perform installation but instead display what command line to use to consistently install + currently requested version of dotnet cli. In example if you specify version 'latest' it will display a link + with specific version so that this command can be used deterministicly in a build script. + It also displays binaries location if you prefer to install or download it yourself. +.PARAMETER NoPath + By default this script will set environment variable PATH for the current process to the binaries folder inside installation folder. + If set it will display binaries location but not set any environment variable. +.PARAMETER Verbose + Displays diagnostics information. +.PARAMETER AzureFeed + Default: https://dotnetcli.azureedge.net/dotnet + This parameter typically is not changed by the user. + It allows changing the URL for the Azure feed used by this installer. +.PARAMETER UncachedFeed + This parameter typically is not changed by the user. + It allows changing the URL for the Uncached feed used by this installer. +.PARAMETER FeedCredential + Used as a query string to append to the Azure feed. + It allows changing the URL to use non-public blob storage accounts. +.PARAMETER ProxyAddress + If set, the installer will use the proxy when making web requests +.PARAMETER ProxyUseDefaultCredentials + Default: false + Use default credentials, when using proxy address. +.PARAMETER SkipNonVersionedFiles + Default: false + Skips installing non-versioned files if they already exist, such as dotnet.exe. +.PARAMETER NoCdn + Disable downloading from the Azure CDN, and use the uncached feed directly. +.PARAMETER JSonFile + Determines the SDK version from a user specified global.json file + Note: global.json must have a value for 'SDK:Version' +#> +[cmdletbinding()] +param( + [string]$Channel="LTS", + [string]$Version="Latest", + [string]$JSonFile, + [string]$InstallDir="", + [string]$Architecture="", + [ValidateSet("dotnet", "aspnetcore", "windowsdesktop", IgnoreCase = $false)] + [string]$Runtime, + [Obsolete("This parameter may be removed in a future version of this script. The recommended alternative is '-Runtime dotnet'.")] + [switch]$SharedRuntime, + [switch]$DryRun, + [switch]$NoPath, + [string]$AzureFeed="https://dotnetcli.azureedge.net/dotnet", + [string]$UncachedFeed="https://dotnetcli.blob.core.windows.net/dotnet", + [string]$FeedCredential, + [string]$ProxyAddress, + [switch]$ProxyUseDefaultCredentials, + [switch]$SkipNonVersionedFiles, + [switch]$NoCdn +) + +Set-StrictMode -Version Latest +$ErrorActionPreference="Stop" +$ProgressPreference="SilentlyContinue" + +if ($NoCdn) { + $AzureFeed = $UncachedFeed +} + +$BinFolderRelativePath="" + +if ($SharedRuntime -and (-not $Runtime)) { + $Runtime = "dotnet" +} + +# example path with regex: shared/1.0.0-beta-12345/somepath +$VersionRegEx="/\d+\.\d+[^/]+/" +$OverrideNonVersionedFiles = !$SkipNonVersionedFiles + +function Say($str) { + Write-Host "dotnet-install: $str" +} + +function Say-Verbose($str) { + Write-Verbose "dotnet-install: $str" +} + +function Say-Invocation($Invocation) { + $command = $Invocation.MyCommand; + $args = (($Invocation.BoundParameters.Keys | foreach { "-$_ `"$($Invocation.BoundParameters[$_])`"" }) -join " ") + Say-Verbose "$command $args" +} + +function Invoke-With-Retry([ScriptBlock]$ScriptBlock, [int]$MaxAttempts = 3, [int]$SecondsBetweenAttempts = 1) { + $Attempts = 0 + + while ($true) { + try { + return $ScriptBlock.Invoke() + } + catch { + $Attempts++ + if ($Attempts -lt $MaxAttempts) { + Start-Sleep $SecondsBetweenAttempts + } + else { + throw + } + } + } +} + +function Get-Machine-Architecture() { + Say-Invocation $MyInvocation + + # possible values: amd64, x64, x86, arm64, arm + return $ENV:PROCESSOR_ARCHITECTURE +} + +function Get-CLIArchitecture-From-Architecture([string]$Architecture) { + Say-Invocation $MyInvocation + + switch ($Architecture.ToLower()) { + { $_ -eq "" } { return Get-CLIArchitecture-From-Architecture $(Get-Machine-Architecture) } + { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } + { $_ -eq "x86" } { return "x86" } + { $_ -eq "arm" } { return "arm" } + { $_ -eq "arm64" } { return "arm64" } + default { throw "Architecture not supported. If you think this is a bug, report it at https://github.com/dotnet/sdk/issues" } + } +} + +# The version text returned from the feeds is a 1-line or 2-line string: +# For the SDK and the dotnet runtime (2 lines): +# Line 1: # commit_hash +# Line 2: # 4-part version +# For the aspnetcore runtime (1 line): +# Line 1: # 4-part version +function Get-Version-Info-From-Version-Text([string]$VersionText) { + Say-Invocation $MyInvocation + + $Data = -split $VersionText + + $VersionInfo = @{ + CommitHash = $(if ($Data.Count -gt 1) { $Data[0] }) + Version = $Data[-1] # last line is always the version number. + } + return $VersionInfo +} + +function Load-Assembly([string] $Assembly) { + try { + Add-Type -Assembly $Assembly | Out-Null + } + catch { + # On Nano Server, Powershell Core Edition is used. Add-Type is unable to resolve base class assemblies because they are not GAC'd. + # Loading the base class assemblies is not unnecessary as the types will automatically get resolved. + } +} + +function GetHTTPResponse([Uri] $Uri) +{ + Invoke-With-Retry( + { + + $HttpClient = $null + + try { + # HttpClient is used vs Invoke-WebRequest in order to support Nano Server which doesn't support the Invoke-WebRequest cmdlet. + Load-Assembly -Assembly System.Net.Http + + if(-not $ProxyAddress) { + try { + # Despite no proxy being explicitly specified, we may still be behind a default proxy + $DefaultProxy = [System.Net.WebRequest]::DefaultWebProxy; + if($DefaultProxy -and (-not $DefaultProxy.IsBypassed($Uri))) { + $ProxyAddress = $DefaultProxy.GetProxy($Uri).OriginalString + $ProxyUseDefaultCredentials = $true + } + } catch { + # Eat the exception and move forward as the above code is an attempt + # at resolving the DefaultProxy that may not have been a problem. + $ProxyAddress = $null + Say-Verbose("Exception ignored: $_.Exception.Message - moving forward...") + } + } + + if($ProxyAddress) { + $HttpClientHandler = New-Object System.Net.Http.HttpClientHandler + $HttpClientHandler.Proxy = New-Object System.Net.WebProxy -Property @{Address=$ProxyAddress;UseDefaultCredentials=$ProxyUseDefaultCredentials} + $HttpClient = New-Object System.Net.Http.HttpClient -ArgumentList $HttpClientHandler + } + else { + + $HttpClient = New-Object System.Net.Http.HttpClient + } + # Default timeout for HttpClient is 100s. For a 50 MB download this assumes 500 KB/s average, any less will time out + # 20 minutes allows it to work over much slower connections. + $HttpClient.Timeout = New-TimeSpan -Minutes 20 + $Response = $HttpClient.GetAsync("${Uri}${FeedCredential}").Result + if (($Response -eq $null) -or (-not ($Response.IsSuccessStatusCode))) { + # The feed credential is potentially sensitive info. Do not log FeedCredential to console output. + $ErrorMsg = "Failed to download $Uri." + if ($Response -ne $null) { + $ErrorMsg += " $Response" + } + + throw $ErrorMsg + } + + return $Response + } + finally { + if ($HttpClient -ne $null) { + $HttpClient.Dispose() + } + } + }) +} + +function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel, [bool]$Coherent) { + Say-Invocation $MyInvocation + + $VersionFileUrl = $null + if ($Runtime -eq "dotnet") { + $VersionFileUrl = "$UncachedFeed/Runtime/$Channel/latest.version" + } + elseif ($Runtime -eq "aspnetcore") { + $VersionFileUrl = "$UncachedFeed/aspnetcore/Runtime/$Channel/latest.version" + } + # Currently, the WindowsDesktop runtime is manufactured with the .Net core runtime + elseif ($Runtime -eq "windowsdesktop") { + $VersionFileUrl = "$UncachedFeed/Runtime/$Channel/latest.version" + } + elseif (-not $Runtime) { + if ($Coherent) { + $VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.coherent.version" + } + else { + $VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.version" + } + } + else { + throw "Invalid value for `$Runtime" + } + try { + $Response = GetHTTPResponse -Uri $VersionFileUrl + } + catch { + throw "Could not resolve version information." + } + $StringContent = $Response.Content.ReadAsStringAsync().Result + + switch ($Response.Content.Headers.ContentType) { + { ($_ -eq "application/octet-stream") } { $VersionText = $StringContent } + { ($_ -eq "text/plain") } { $VersionText = $StringContent } + { ($_ -eq "text/plain; charset=UTF-8") } { $VersionText = $StringContent } + default { throw "``$Response.Content.Headers.ContentType`` is an unknown .version file content type." } + } + + $VersionInfo = Get-Version-Info-From-Version-Text $VersionText + + return $VersionInfo +} + +function Parse-Jsonfile-For-Version([string]$JSonFile) { + Say-Invocation $MyInvocation + + If (-Not (Test-Path $JSonFile)) { + throw "Unable to find '$JSonFile'" + exit 0 + } + try { + $JSonContent = Get-Content($JSonFile) -Raw | ConvertFrom-Json | Select-Object -expand "sdk" -ErrorAction SilentlyContinue + } + catch { + throw "Json file unreadable: '$JSonFile'" + exit 0 + } + if ($JSonContent) { + try { + $JSonContent.PSObject.Properties | ForEach-Object { + $PropertyName = $_.Name + if ($PropertyName -eq "version") { + $Version = $_.Value + Say-Verbose "Version = $Version" + } + } + } + catch { + throw "Unable to parse the SDK node in '$JSonFile'" + exit 0 + } + } + else { + throw "Unable to find the SDK node in '$JSonFile'" + exit 0 + } + If ($Version -eq $null) { + throw "Unable to find the SDK:version node in '$JSonFile'" + exit 0 + } + return $Version +} + +function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel, [string]$Version, [string]$JSonFile) { + Say-Invocation $MyInvocation + + if (-not $JSonFile) { + switch ($Version.ToLower()) { + { $_ -eq "latest" } { + $LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $False + return $LatestVersionInfo.Version + } + { $_ -eq "coherent" } { + $LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $True + return $LatestVersionInfo.Version + } + default { return $Version } + } + } + else { + return Parse-Jsonfile-For-Version $JSonFile + } +} + +function Get-Download-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + if ($Runtime -eq "dotnet") { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificVersion-win-$CLIArchitecture.zip" + } + elseif ($Runtime -eq "aspnetcore") { + $PayloadURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$SpecificVersion-win-$CLIArchitecture.zip" + } + elseif ($Runtime -eq "windowsdesktop") { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/windowsdesktop-runtime-$SpecificVersion-win-$CLIArchitecture.zip" + } + elseif (-not $Runtime) { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificVersion-win-$CLIArchitecture.zip" + } + else { + throw "Invalid value for `$Runtime" + } + + Say-Verbose "Constructed primary named payload URL: $PayloadURL" + + return $PayloadURL +} + +function Get-LegacyDownload-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + if (-not $Runtime) { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-dev-win-$CLIArchitecture.$SpecificVersion.zip" + } + elseif ($Runtime -eq "dotnet") { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-win-$CLIArchitecture.$SpecificVersion.zip" + } + else { + return $null + } + + Say-Verbose "Constructed legacy named payload URL: $PayloadURL" + + return $PayloadURL +} + +function Get-User-Share-Path() { + Say-Invocation $MyInvocation + + $InstallRoot = $env:DOTNET_INSTALL_DIR + if (!$InstallRoot) { + $InstallRoot = "$env:LocalAppData\Microsoft\dotnet" + } + return $InstallRoot +} + +function Resolve-Installation-Path([string]$InstallDir) { + Say-Invocation $MyInvocation + + if ($InstallDir -eq "") { + return Get-User-Share-Path + } + return $InstallDir +} + +function Is-Dotnet-Package-Installed([string]$InstallRoot, [string]$RelativePathToPackage, [string]$SpecificVersion) { + Say-Invocation $MyInvocation + + $DotnetPackagePath = Join-Path -Path $InstallRoot -ChildPath $RelativePathToPackage | Join-Path -ChildPath $SpecificVersion + Say-Verbose "Is-Dotnet-Package-Installed: Path to a package: $DotnetPackagePath" + return Test-Path $DotnetPackagePath -PathType Container +} + +function Get-Absolute-Path([string]$RelativeOrAbsolutePath) { + # Too much spam + # Say-Invocation $MyInvocation + + return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($RelativeOrAbsolutePath) +} + +function Get-Path-Prefix-With-Version($path) { + $match = [regex]::match($path, $VersionRegEx) + if ($match.Success) { + return $entry.FullName.Substring(0, $match.Index + $match.Length) + } + + return $null +} + +function Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package([System.IO.Compression.ZipArchive]$Zip, [string]$OutPath) { + Say-Invocation $MyInvocation + + $ret = @() + foreach ($entry in $Zip.Entries) { + $dir = Get-Path-Prefix-With-Version $entry.FullName + if ($dir -ne $null) { + $path = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $dir) + if (-Not (Test-Path $path -PathType Container)) { + $ret += $dir + } + } + } + + $ret = $ret | Sort-Object | Get-Unique + + $values = ($ret | foreach { "$_" }) -join ";" + Say-Verbose "Directories to unpack: $values" + + return $ret +} + +# Example zip content and extraction algorithm: +# Rule: files if extracted are always being extracted to the same relative path locally +# .\ +# a.exe # file does not exist locally, extract +# b.dll # file exists locally, override only if $OverrideFiles set +# aaa\ # same rules as for files +# ... +# abc\1.0.0\ # directory contains version and exists locally +# ... # do not extract content under versioned part +# abc\asd\ # same rules as for files +# ... +# def\ghi\1.0.1\ # directory contains version and does not exist locally +# ... # extract content +function Extract-Dotnet-Package([string]$ZipPath, [string]$OutPath) { + Say-Invocation $MyInvocation + + Load-Assembly -Assembly System.IO.Compression.FileSystem + Set-Variable -Name Zip + try { + $Zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) + + $DirectoriesToUnpack = Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package -Zip $Zip -OutPath $OutPath + + foreach ($entry in $Zip.Entries) { + $PathWithVersion = Get-Path-Prefix-With-Version $entry.FullName + if (($PathWithVersion -eq $null) -Or ($DirectoriesToUnpack -contains $PathWithVersion)) { + $DestinationPath = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $entry.FullName) + $DestinationDir = Split-Path -Parent $DestinationPath + $OverrideFiles=$OverrideNonVersionedFiles -Or (-Not (Test-Path $DestinationPath)) + if ((-Not $DestinationPath.EndsWith("\")) -And $OverrideFiles) { + New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $DestinationPath, $OverrideNonVersionedFiles) + } + } + } + } + finally { + if ($Zip -ne $null) { + $Zip.Dispose() + } + } +} + +function DownloadFile($Source, [string]$OutPath) { + if ($Source -notlike "http*") { + # Using System.IO.Path.GetFullPath to get the current directory + # does not work in this context - $pwd gives the current directory + if (![System.IO.Path]::IsPathRooted($Source)) { + $Source = $(Join-Path -Path $pwd -ChildPath $Source) + } + $Source = Get-Absolute-Path $Source + Say "Copying file from $Source to $OutPath" + Copy-Item $Source $OutPath + return + } + + $Stream = $null + + try { + $Response = GetHTTPResponse -Uri $Source + $Stream = $Response.Content.ReadAsStreamAsync().Result + $File = [System.IO.File]::Create($OutPath) + $Stream.CopyTo($File) + $File.Close() + } + finally { + if ($Stream -ne $null) { + $Stream.Dispose() + } + } +} + +function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot, [string]$BinFolderRelativePath) { + $BinPath = Get-Absolute-Path $(Join-Path -Path $InstallRoot -ChildPath $BinFolderRelativePath) + if (-Not $NoPath) { + $SuffixedBinPath = "$BinPath;" + if (-Not $env:path.Contains($SuffixedBinPath)) { + Say "Adding to current process PATH: `"$BinPath`". Note: This change will not be visible if PowerShell was run as a child process." + $env:path = $SuffixedBinPath + $env:path + } else { + Say-Verbose "Current process PATH already contains `"$BinPath`"" + } + } + else { + Say "Binaries of dotnet can be found in $BinPath" + } +} + +$CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture +$SpecificVersion = Get-Specific-Version-From-Version -AzureFeed $AzureFeed -Channel $Channel -Version $Version -JSonFile $JSonFile +$DownloadLink = Get-Download-Link -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture +$LegacyDownloadLink = Get-LegacyDownload-Link -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture + +$InstallRoot = Resolve-Installation-Path $InstallDir +Say-Verbose "InstallRoot: $InstallRoot" +$ScriptName = $MyInvocation.MyCommand.Name + +if ($DryRun) { + Say "Payload URLs:" + Say "Primary named payload URL: $DownloadLink" + if ($LegacyDownloadLink) { + Say "Legacy named payload URL: $LegacyDownloadLink" + } + $RepeatableCommand = ".\$ScriptName -Version `"$SpecificVersion`" -InstallDir `"$InstallRoot`" -Architecture `"$CLIArchitecture`"" + if ($Runtime -eq "dotnet") { + $RepeatableCommand+=" -Runtime `"dotnet`"" + } + elseif ($Runtime -eq "aspnetcore") { + $RepeatableCommand+=" -Runtime `"aspnetcore`"" + } + foreach ($key in $MyInvocation.BoundParameters.Keys) { + if (-not (@("Architecture","Channel","DryRun","InstallDir","Runtime","SharedRuntime","Version") -contains $key)) { + $RepeatableCommand+=" -$key `"$($MyInvocation.BoundParameters[$key])`"" + } + } + Say "Repeatable invocation: $RepeatableCommand" + exit 0 +} + +if ($Runtime -eq "dotnet") { + $assetName = ".NET Core Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.NETCore.App" +} +elseif ($Runtime -eq "aspnetcore") { + $assetName = "ASP.NET Core Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.AspNetCore.App" +} +elseif ($Runtime -eq "windowsdesktop") { + $assetName = ".NET Core Windows Desktop Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.WindowsDesktop.App" +} +elseif (-not $Runtime) { + $assetName = ".NET Core SDK" + $dotnetPackageRelativePath = "sdk" +} +else { + throw "Invalid value for `$Runtime" +} + +# Check if the SDK version is already installed. +$isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $SpecificVersion +if ($isAssetInstalled) { + Say "$assetName version $SpecificVersion is already installed." + Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath + exit 0 +} + +New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null + +$installDrive = $((Get-Item $InstallRoot).PSDrive.Name); +$diskInfo = Get-PSDrive -Name $installDrive +if ($diskInfo.Free / 1MB -le 100) { + Say "There is not enough disk space on drive ${installDrive}:" + exit 0 +} + +$ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()) +Say-Verbose "Zip path: $ZipPath" + +$DownloadFailed = $false +Say "Downloading link: $DownloadLink" +try { + DownloadFile -Source $DownloadLink -OutPath $ZipPath +} +catch { + Say "Cannot download: $DownloadLink" + if ($LegacyDownloadLink) { + $DownloadLink = $LegacyDownloadLink + $ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()) + Say-Verbose "Legacy zip path: $ZipPath" + Say "Downloading legacy link: $DownloadLink" + try { + DownloadFile -Source $DownloadLink -OutPath $ZipPath + } + catch { + Say "Cannot download: $DownloadLink" + $DownloadFailed = $true + } + } + else { + $DownloadFailed = $true + } +} + +if ($DownloadFailed) { + throw "Could not find/download: `"$assetName`" with version = $SpecificVersion`nRefer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support" +} + +Say "Extracting zip from $DownloadLink" +Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot + +# Check if the SDK version is now installed; if not, fail the installation. +$isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $SpecificVersion +if (!$isAssetInstalled) { + throw "`"$assetName`" with version = $SpecificVersion failed to install with an unknown error." +} + +Remove-Item $ZipPath + +Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath + +Say "Installation finished" +exit 0