-
-
Save bentman/638c478ae791598780c70749139e382f to your computer and use it in GitHub Desktop.
| <# | |
| .SYNOPSIS | |
| Script to install Dev Tools on Windows Server (tested on 2022) | |
| .DESCRIPTION | |
| Reference script (gist) for manual execution on Windows Server 2022+. | |
| Requires user interaction for installations. Installs dev tools in order: | |
| 1. Microsoft.VCLibs (latest via GitHub winget-cli deps) | |
| 2. Microsoft.UI.Xaml (latest via GitHub API) | |
| 3. winget-cli (latest via GitHub API) | |
| 4. Microsoft.WindowsTerminal (latest via GitHub API) | |
| NOTE: Installing from winget, now requires shell restart after winget install. | |
| 5. Microsoft pwsh.exe (current via winget) | |
| 6. Microsoft VSCode (current via winget) | |
| 7. Azure CLI (current via winget) | |
| Also configures SSH and RDP. | |
| .NOTES | |
| Add-DevToMyWinServer.ps1 | |
| Version: 1.6 | |
| Creation Date: 2024-05-04 | |
| Updated: 2026-01-14 | |
| Author: https://github.com/bentman | |
| #> | |
| Push-Location ~\Downloads | |
| # Install NuGet (no-prompt) & set PSGallery trusted | |
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | |
| Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | |
| Set-PSRepository -Name PSGallery -InstallationPolicy Trusted | |
| # Detect system architecture | |
| $systemArch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture | |
| $archMap = @{ | |
| 'X64' = @{ Arch = 'x64'; PackageArch = 'x64' } | |
| 'Arm64' = @{ Arch = 'arm64'; PackageArch = 'arm' } # AppX packages use 'arm' for ARM64 due to Microsoft's naming convention | |
| 'X86' = @{ Arch = 'x86'; PackageArch = 'x86' } | |
| } | |
| if ($archMap.ContainsKey($systemArch)) { | |
| $arch = $archMap[$systemArch].Arch | |
| $packageArch = $archMap[$systemArch].PackageArch | |
| } | |
| else { throw "Unsupported architecture: $systemArch" } | |
| # Validate package architecture | |
| if (-not $packageArch) { throw "Failed to determine package architecture for $arch." } | |
| Write-Host "Detected architecture: $arch (using '$packageArch' for package names)" | |
| Write-Verbose "System architecture detected as $systemArch" | |
| ### 1. Install Microsoft.VCLibs (dependency for UI.Xaml and WinGet) | |
| $latestBase = 'https://github.com/microsoft/winget-cli/releases/latest/download' # Base URL for latest winget-cli release downloads | |
| $depsZip = 'DesktopAppInstaller_Dependencies.zip' # Name of dependencies zip file | |
| $depsOut = Join-Path $PWD $depsZip # Local path for deps zip | |
| $depsDir = Join-Path $PWD 'wingetDeps' # Directory for extracted deps | |
| Invoke-WebRequest -Uri "$latestBase/$depsZip" -OutFile $depsOut -UseBasicParsing # Download deps zip | |
| Unblock-File $depsOut # Unblock zip file | |
| if (-not (Test-Path $depsDir)) { New-Item -Path $depsDir -ItemType Directory -Force } # Create extraction dir | |
| Expand-Archive -LiteralPath $depsOut -DestinationPath $depsDir -Force # Extract zip | |
| $appxVCLibs = Get-ChildItem -Path (Join-Path $depsDir $packageArch) -Filter 'Microsoft.VCLibs*.appx' | Select-Object -First 1 # Find VCLibs appx file | |
| if (-not $appxVCLibs) { throw "VCLibs package for $packageArch not found." } # Validate VCLibs found | |
| Write-Host "Installing VCLibs from $($appxVCLibs.FullName)" # Inform user of installation | |
| Add-AppxPackage -Path $appxVCLibs.FullName -ForceApplicationShutdown # Install VCLibs | |
| ### 2. Install Microsoft.WindowsAppRuntime (dependency for WinGet) | |
| $winAppSdkRepo = "https://api.github.com/repos/microsoft/WindowsAppSDK/releases/latest" # URL for latest WindowsAppSDK release | |
| $winAppSdkRelease = Invoke-WebRequest -Uri $winAppSdkRepo -UseBasicParsing | ConvertFrom-Json # Fetch latest release info | |
| $versionMatch = $winAppSdkRelease.body | Select-String -Pattern 'Microsoft\.WindowsAppSDK/(\d+\.\d+\.\d+)' # Extract version from release body | |
| if (-not $versionMatch) { throw "Could not find WindowsAppSDK version in release body." } # Validate version found | |
| $fullVersion = $versionMatch.Matches[0].Groups[1].Value # Get full version string | |
| $majorMinor = $fullVersion -replace '\.\d+$', '' # Extract major.minor version | |
| $winAppRunUrl = "https://aka.ms/windowsappsdk/$majorMinor/$fullVersion/WindowsAppRuntimeInstall-$arch.exe" # Construct download URL | |
| $winAppRunName = Join-Path $PWD "WindowsAppRuntimeInstall-$arch.exe" # Local file path | |
| Write-Host "Downloading WindowsAppRuntime installer ($arch) from $winAppRunUrl" # Inform user of download | |
| Invoke-WebRequest -Uri $winAppRunUrl -OutFile $winAppRunName -UseBasicParsing # Download installer | |
| Unblock-File $winAppRunName # Unblock installer file | |
| Write-Host "Installing WindowsAppRuntime from $winAppRunName" # Inform user of installation | |
| Start-Process -FilePath $winAppRunName -Wait # Install WindowsAppRuntime | |
| ### 3. Install WinGet (winget-cli) (depends on VCLibs and UI.Xaml) | |
| $winGetRepo = "https://api.github.com/repos/microsoft/winget-cli/releases/latest" # URL for latest winget-cli release | |
| $release = Invoke-WebRequest -Uri $winGetRepo -UseBasicParsing | ConvertFrom-Json # Fetch latest release | |
| $licXmlLink = $release.assets | ` | |
| Where-Object browser_download_url -Match '_License1.xml' | ` | |
| Select-Object -ExpandProperty browser_download_url # Find license XML download URL | |
| $licXmlName = '_License1.xml' # Local name for license file | |
| Invoke-WebRequest -Uri $licXmlLink -OutFile $licXmlName -UseBasicParsing # Download license | |
| Unblock-File .\$licXmlName # Unblock license file | |
| $winGetLink = $release.assets | Where-Object browser_download_url -Match '.msixbundle' | ` | |
| Select-Object -ExpandProperty browser_download_url # Find WinGet msixbundle download URL | |
| $winGetName = "winget.msixbundle" # Local name for WinGet bundle | |
| Invoke-WebRequest -Uri $winGetLink -OutFile $winGetName -UseBasicParsing # Download WinGet bundle | |
| Unblock-File .\$winGetName # Unblock bundle file | |
| Add-AppxProvisionedPackage -Online -PackagePath .\$winGetName -LicensePath .\$licXmlName -Verbose # Install WinGet | |
| ### 4. Install Windows Terminal (depends on VCLibs and UI.Xaml) | |
| $termRepo = "https://api.github.com/repos/microsoft/terminal/releases/latest" # URL for latest Windows Terminal release | |
| $termRel = Invoke-WebRequest -Uri $termRepo -UseBasicParsing | ConvertFrom-Json # Fetch latest release | |
| $termLink = $termRel.assets | Where-Object { # Find the appropriate msixbundle for Windows Terminal | |
| $_.browser_download_url -match '\.msixbundle$' -and $_.browser_download_url -notmatch 'Windows10_PreinstallKit' # Exclude Windows 10 preinstall kits | |
| } | Select-Object -ExpandProperty browser_download_url # Get download URL | |
| $termName = 'WindowsTerminal.msixbundle' # Local name for Terminal bundle | |
| Invoke-WebRequest -Uri $termLink -OutFile .\$termName -Verbose # Download Terminal bundle | |
| Unblock-File .\$termName # Unblock bundle file | |
| Add-AppPackage -Path .\$termName -Verbose # Install Windows Terminal | |
| Pop-Location | |
| ################################################################################################################# | |
| ### NOTE: Shell restart required after WinGet installation for winget command to be available in new sessions ### | |
| ################################################################################################################# | |
| # 5. Install PowerShell 7 via winget (requires winget from step 3) | |
| # Search for available PowerShell versions (may prompt to accept terms) | |
| winget search Microsoft.PowerShell | |
| # Install PowerShell 7 | |
| winget install --id Microsoft.Powershell --source winget | |
| # 6. Install VS Code via winget | |
| # Search for available VS Code versions (may prompt to accept terms) | |
| winget search Microsoft.VisualStudioCode | |
| # Install VS Code | |
| winget install --id Microsoft.VisualStudioCode --source winget | |
| # 7. Install Azure CLI via winget | |
| # Search for available Azure CLI versions (may prompt to accept terms) | |
| winget search Microsoft.AzureCLI | |
| # Install Azure CLI | |
| winget install --id Microsoft.AzureCLI --source winget | |
| # 8. Update all installed packages via winget | |
| winget update --all | |
| # 9. Disable NLA on RDP (optional, not recommended for production) | |
| Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name 'UserAuthentication' -Value 0 | |
| ### SSH Configuration ### | |
| # Install OpenSSH Client and Server | |
| Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 | |
| Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 | |
| # Start and configure SSH Server Service | |
| Get-Service sshd | Start-Service # Start SSH service | |
| Set-Service -Name sshd -StartupType 'Automatic' # Set to auto-start | |
| # Configure SSH Server firewall rule | |
| $sshRule = Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue | |
| if (-not $sshRule) { | |
| New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 # Create firewall rule for SSH | |
| } | |
| elseif (-not $sshRule.Enabled) { | |
| Set-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -Enabled True # Enable existing rule | |
| } | |
| # Set PowerShell 7 as default SSH shell (if available) | |
| $pwshPath = (Get-Command pwsh -ErrorAction SilentlyContinue).Source | |
| if ($pwshPath) { | |
| New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value "`"$pwshPath`"" -PropertyType String -Force # Set pwsh as default shell | |
| Write-Host "SSH default shell set to: $pwshPath" | |
| } | |
| else { | |
| # Fallback to Windows PowerShell 5.1 | |
| New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force # Set PowerShell 5.1 as default | |
| Write-Host "SSH default shell set to: Windows PowerShell 5.1" | |
| } |
Updated versions...
winget-cli v1.7.11132 (github)
Microsoft.WindowsTerminal v1.19.11213.0 (github)
VERBOSE: GET https://github.com/microsoft/terminal/releases/download/v1.19.11213.0/Microsoft.WindowsTerminal_1.19.11213.0_8wekyb3d8bbwe.msixbundle with 0-byte payload
VERBOSE: received 21550188-byte response of content type application/octet-stream
VERBOSE: Performing the operation "Deploy package" on target "D:\Microsoft.WindowsTerminal_1.19.11213.0_8wekyb3d8bbwe.msixbundle".
Add-AppPackage : Deployment failed with HRESULT: 0x80073CF3, Package failed updates, dependency or conflict validation.
Windows cannot install package Microsoft.WindowsTerminal_1.19.11213.0_x64__8wekyb3d8bbwe because this package depends on a framework that could not be found. Provide the framework "Microsoft.UI.Xaml.2.8" published by "CN=Microsof
t Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", with neutral or x64 processor architecture and minimum version 8.2305.5001.0, along with this package to install. The frameworks with name "Microsoft.UI.Xaml
.2.8" currently installed are: {}
fail on Windows Server 2022
should upgrade to 2.8:
$MsftUi_Link = 'https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx'
> fail on Windows Server 2022
should upgrade to 2.8:
$MsftUi_Link = 'https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx'
yup. looking for a way to do them all more 'dynamically' (aka - query the latest version to download). i'll update the static until then. thanks!
Updated dynamic version retrieval...
winget-cli (api.github.com)
Microsoft.WindowsTerminal (api.github.com)
Updated to v1.5
- System Architecture handling
- Compensates for the 'moving targets' (VCLibs & UI.Xaml)
-- VCLibs aka.ms download is 'version-locked', now getting directly from winget deps
-- Updated UI.Xaml to use dynamic version retrieval
Thanks! This rules!