DeveloperEnvironment.ps1
|
function Test-VsEnvironmentIdentity { [CmdletBinding()] param( [Parameter(Mandatory)] $Installation, [Parameter(Mandatory)] [string]$Architecture, [Parameter(Mandatory)] [string]$HostArchitecture, [Parameter(Mandatory)] [System.Collections.IDictionary]$Environment ) $markers = @('VSCMD_VER', 'VSINSTALLDIR', 'VSCMD_ARG_TGT_ARCH', 'VSCMD_ARG_HOST_ARCH') $present = @($markers | Where-Object { $Environment[$_] }) if ($present.Count -eq 0) { return $false } $activeVersion = $null $matches = $present.Count -eq $markers.Count -and [version]::TryParse([string]$Environment['VSCMD_VER'], [ref]$activeVersion) -and $activeVersion.Major -eq ([version]$Installation.Version).Major -and [IO.Path]::IsPathFullyQualified([string]$Environment['VSINSTALLDIR']) if ($matches) { $activePath = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath([string]$Environment['VSINSTALLDIR'])) $requestedPath = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath([string]$Installation.InstallationPath)) $matches = $activePath -ieq $requestedPath -and ($Environment['VSCMD_ARG_TGT_ARCH'] -ireplace '^x64$', 'amd64') -ieq $Architecture -and ($Environment['VSCMD_ARG_HOST_ARCH'] -ireplace '^x64$', 'amd64') -ieq $HostArchitecture } if (-not $matches) { throw 'A different or incomplete Visual Studio environment is already active. Start a fresh Start-DevShell session; switching or stacking toolsets is not supported.' } $true } function Get-VsEnvironmentSnapshot { [CmdletBinding()] param() $snapshot = @{} foreach ($entry in [Environment]::GetEnvironmentVariables('Process').GetEnumerator()) { $snapshot[[string]$entry.Key] = [string]$entry.Value } $snapshot } function Invoke-VsEnvironmentCapture { [CmdletBinding()] param( [Parameter(Mandatory)] $Installation, [Parameter(Mandatory)] [ValidateSet('x86', 'amd64', 'arm', 'arm64')] [string]$Architecture, [Parameter(Mandatory)] [ValidateSet('x86', 'amd64', 'arm64')] [string]$HostArchitecture ) $pwsh = Join-Path $PSHOME 'pwsh.exe' foreach ($path in @($Installation.ScriptPath, $pwsh)) { if (-not [IO.Path]::IsPathFullyQualified($path) -or $path -match '[%!^&|<>"\r\n]' -or -not (Test-Path -LiteralPath $path -PathType Leaf -ErrorAction Stop)) { throw "Cannot safely initialize Visual Studio using path '$path'. A fully qualified existing path without cmd.exe expansion or control characters is required." } } $capture = @' $ErrorActionPreference = 'Stop' $values = @{} foreach ($entry in [Environment]::GetEnvironmentVariables('Process').GetEnumerator()) { if ($entry.Key -ne 'SHMUELIE_VS_CAPTURE') { $values[[string]$entry.Key] = [string]$entry.Value } } [IO.File]::WriteAllText($env:SHMUELIE_VS_CAPTURE, ($values | ConvertTo-Json -Compress), [Text.UTF8Encoding]::new($false)) '@ $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($capture)) $resultFile = [IO.Path]::GetTempFileName() $process = [Diagnostics.Process]::new() try { $start = [Diagnostics.ProcessStartInfo]::new() $start.FileName = Join-Path ([Environment]::SystemDirectory) 'cmd.exe' $start.UseShellExecute = $false $start.CreateNoWindow = $true $start.RedirectStandardOutput = $true $start.RedirectStandardError = $true $start.Environment['SHMUELIE_VS_CAPTURE'] = $resultFile $start.Arguments = '/d /s /c ""{0}" -no_logo -arch={1} -host_arch={2} && "{3}" -NoLogo -NoProfile -NonInteractive -EncodedCommand {4}"' -f $Installation.ScriptPath, $Architecture, $HostArchitecture, $pwsh, $encoded $process.StartInfo = $start if (-not $process.Start()) { throw 'Could not start the Visual Studio environment initializer.' } $stdout = $process.StandardOutput.ReadToEndAsync() $stderr = $process.StandardError.ReadToEndAsync() if (-not $process.WaitForExit(60000)) { $process.Kill($true) $process.WaitForExit() throw 'Visual Studio environment initialization timed out after 60 seconds.' } $output = $stdout.GetAwaiter().GetResult() $errors = $stderr.GetAwaiter().GetResult() if ($process.ExitCode -ne 0) { throw "Visual Studio environment initialization exited with code $($process.ExitCode). $errors $output" } $json = [IO.File]::ReadAllText($resultFile) if (-not $json) { throw 'Visual Studio environment initialization returned no environment.' } $environment = ConvertFrom-Json -InputObject $json -AsHashtable -ErrorAction Stop if ($environment -isnot [System.Collections.IDictionary] -or $environment.Count -eq 0) { throw 'Visual Studio environment initialization returned an invalid environment.' } foreach ($key in $environment.Keys) { if (-not $key -or $key -match '[=\x00]' -or $environment[$key] -isnot [string] -or $environment[$key].Contains([char]0)) { throw 'Visual Studio environment initialization returned an invalid environment entry.' } } $environment } finally { $process.Dispose() [IO.File]::Delete($resultFile) } } function Initialize-VsDeveloperEnvironment { [CmdletBinding()] param( [Parameter(Mandatory)] $Installation, [Parameter(Mandatory)] [ValidateSet('x86', 'amd64', 'arm', 'arm64')] [string]$Architecture, [Parameter(Mandatory)] [ValidateSet('x86', 'amd64', 'arm64')] [string]$HostArchitecture ) $before = Get-VsEnvironmentSnapshot if (Test-VsEnvironmentIdentity -Installation $Installation -Architecture $Architecture -HostArchitecture $HostArchitecture -Environment $before) { return } $after = Invoke-VsEnvironmentCapture -Installation $Installation -Architecture $Architecture -HostArchitecture $HostArchitecture if (-not (Test-VsEnvironmentIdentity -Installation $Installation -Architecture $Architecture -HostArchitecture $HostArchitecture -Environment $after)) { throw 'Visual Studio initialization did not establish the requested environment.' } $originals = @{} try { foreach ($key in @($before.Keys) + @($after.Keys) | Sort-Object -Unique) { if ($key -in @('PSModulePath', 'SHMUELIE_VS_CAPTURE') -or $before[$key] -ceq $after[$key]) { continue } $originals[$key] = $before[$key] [Environment]::SetEnvironmentVariable($key, $after[$key], 'Process') } } catch { $applicationError = $_ foreach ($key in $originals.Keys) { [Environment]::SetEnvironmentVariable($key, $originals[$key], 'Process') } throw $applicationError } $script:VsEnvironmentOriginals = $originals } function Set-VsYearEnvironment { [CmdletBinding()] param( [Parameter(Mandatory)] [int]$Year, [Parameter(Mandatory)] [string]$Architecture, [Parameter(Mandatory)] [string]$HostArchitecture, [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet]$Caller ) Assert-WindowsOnly -CommandName "Set-VS$Year" $installation = Get-VsDeveloperInstallation -Year $Year | Select-Object -First 1 if (-not $installation) { throw "No complete, launchable Visual Studio $Year release installation with VsDevCmd.bat was found." } if ($Caller.ShouldProcess("$($installation.InstallationPath) ($Architecture, host $HostArchitecture)", 'Initialize current Visual Studio developer environment')) { Initialize-VsDeveloperEnvironment -Installation $installation -Architecture $Architecture -HostArchitecture $HostArchitecture } } |