Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a066096
Print PWD to help debugging in `prepare_windows_host_for_node.ps1`
mokagio Feb 3, 2025
ac494f5
Print path at which `certificate.pfx` is saved
mokagio Feb 3, 2025
cb3e3ff
Track command to install Windows 10 SDK
mokagio Feb 13, 2025
51b682b
Add entry for command to install Windows 10 SDK to CHANGELOG
mokagio Feb 14, 2025
2a3539b
Remove logic to install Node.js from "prepare" PS1 command
mokagio Feb 14, 2025
d707ff5
Deprecate prepare_windows_host_for_node in favor of script with new name
mokagio Feb 17, 2025
a7e8550
DRY new PS1 name in deprecated script
mokagio Feb 17, 2025
8dfadff
Remove a leftover debug log in Windows setup script
mokagio Feb 17, 2025
b3cbf3f
Load cert as last action in Windows setup
mokagio Feb 17, 2025
abf5498
Make app distribution setup script install Windows 10 SDK
mokagio Feb 17, 2025
2d93f61
Update `CHANGELOG` with more of the Windows changes
mokagio Feb 17, 2025
41262e8
DRY certificate.bin and certificate.pfx definitions
mokagio Feb 17, 2025
5bb49fb
Add `path_aware_refreshenv.ps1`
mokagio Feb 17, 2025
c84c8ca
Add changelog entry regarding `path_aware_refreshenv.ps1`
mokagio Feb 17, 2025
2656bfa
Completely remove `prepare_windows_host_for_node.ps1`
mokagio Feb 19, 2025
4561513
Add header doc for `prepare_windows_host_for_app_distribution`
mokagio Feb 19, 2025
ab6f9b2
Add header doc to `path_aware_refreshenv.ps1`
mokagio Feb 20, 2025
97bc1ca
Add missing PS1 extension to changelog
mokagio Feb 21, 2025
09b60ef
Add another missing PS1 extension to changelog...
mokagio Feb 21, 2025
18cf230
Fix grammar in `CHANGELOG.md`
mokagio Feb 21, 2025
356fe9b
Refine Windows 10 SDK install command and add tests (#153)
mokagio Feb 26, 2025
854d69d
Fix a typo in the install Win 10 SDK docs
mokagio Feb 28, 2025
984ea7b
Merge branch 'trunk' into mokagio/more-windows-utils
mokagio Feb 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ _None._

### Breaking Changes

_None._
- `prepare_windows_host_for_node.ps1` no longer install Node.js.
Clients should use [`nvm-buildkite-plugin`](https://github.com/Automattic/nvm-buildkite-plugin) instead [#144]

### New Features

_None._
- Add new command to install Windows 10 SDK on Windows CI machines [#144]
- `prepare_windows_host_for_node.ps1` has been deprecated in favor of the better name `prepare_windows_host_for_app_distribution` [#144]
- `prepare_windows_host_for_app_distribution` automatically installs the Windows 10 SDK if version file is found [#144]

### Bug Fixes

Expand Down
59 changes: 59 additions & 0 deletions bin/install_windows_10_sdk.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Stop script execution when a non-terminating error occurs
$ErrorActionPreference = "Stop"

Write-Host "--- :windows: Installing Windows 10 SDK and Visual Studio Build Tools"

$windowsSDKVersionFile = ".windows-10-sdk-version"
if (-not (Test-Path $windowsSDKVersionFile)) {
Write-Output "[!] No Windows 10 SDK version file found at $windowsSDKVersionFile."
exit 1
}

$windows10SDKVersion = Get-Content $windowsSDKVersionFile
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: how does this Get-Content handles possible newlines?

Especially, if the version file ends with a newline at end of file, is that trimmed?
Or maybe it's not, but that doesn't cause any issues because when you then reference $windows10SDKVersion in other commands like --add Microsoft.VisualStudio.Component.Windows10SDK.$windows10SDKVersion that's when the value is trimmed as usage/substitution site?
What if the file contains 2 newlines at end of file? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the -TotalCount 1 parameter of Get-Content can help?
Though even if that allows to limit only reading the first line of the file, not 100% sure if it'd properly trim the \n at the end of that read line, in that case you might need to also use Trim() or TrimEnd().

Tentative suggestion (not tested):

Suggested change
$windows10SDKVersion = Get-Content $windowsSDKVersionFile
$windows10SDKVersion = (Get-Content -TotalCount 1 $windowsSDKVersionFile).Trim()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spent more time than I care to admit on this but got a bunch of tests for the version parsing

image

I'll clean up tomorrow #153


Write-Host "Will attempt to set up Windows 10 ($windows10SDKVersion) SDK and Visual Studio Build Tools..."

# Download the Visual Studio Build Tools Bootstrapper
Write-Output "~~~ Downloading Visual Studio Build Tools..."

$buildToolsPath = ".\vs_buildtools.exe"

Invoke-WebRequest `
-Uri https://aka.ms/vs/17/release/vs_buildtools.exe `
-OutFile $buildToolsPath

If (-not (Test-Path $buildToolsPath)) {
Write-Output "[!] Failed to download Visual Studio Build Tools"
Exit 1
} else {
Write-Output "Successfully downloaded Visual Studio Build Toosl at $buildToolsPath."
}

# Install the Windows SDK and other required components
Write-Output "~~~ Installing Visual Studio Build Tools..."
Start-Process `
-FilePath $buildToolsPath `
-ArgumentList "--quiet --wait --add Microsoft.VisualStudio.Component.Windows10SDK.$windows10SDKVersion" `
-NoNewWindow `
-Wait

# Check if the installation was successful in file system
$windowsSDKsRoot = "C:\Program Files (x86)\Windows Kits\10\bin"
$sdkPath = "$windowsSDKsRoot\10.0.$windows10SDKVersion.0\x64"
If (-not (Test-Path $sdkPath)) {
Write-Output "[!] Failed to install Windows 10 SDK: Could not find SDK at $sdkPath."
If (-not (Test-Path $windowsSDKsRoot)) {
Write-Output "[!] Expected $windowsSDKsRoot to exist, but it does not."
} else {
Write-Output " Found:"
Get-ChildItem -Path $windowsSDKsRoot | ForEach-Object { Write-Output " - $windowsSDKsRoot\$_" }
}
Exit 1
}
Comment on lines +79 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an easy way to check if $windows10SDKVersion is valid (i.e. an existing version, as opposed to if someone tried to use latest or none or whatever invalid text in the version file) and detect it early?

In particular, I'm wondering if the vs_buildtools.exe would fail with an explicit and clear error if that -add … argument we pass to it was invalid (i.e. if it didn't find the component), allowing us to print a specific error message in that case (like "Check the value you set in $windowsSDKVersionFile"), as opposed to the installer happily continuing with the installation of all the other default components—without complaining about this one not existing in the options—and us only finding out the component was not installed after the fact, thanks to your test on lines 42–51… (and even in that case, would it help to provide in the error messages a more explicit suggestion to check the syntax/version used in the version file?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if the vs_buildtools.exe would fail with an explicit and clear error

I've done a few experiments in my VM. It fails silently, with exit code 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll duplicated what I wrote in #153 (comment)

This all makes me wonder whether we might be better off using a default SDK version in the installer, and only falling back to the version file and/or a parameter if the user provides one. After all, I implemented the version file because other tools use it and because the app I was working on had a check for the SDK being installed and so I wanted a single source of truth. But I'd argue now that our tooling takes care of the installing, that check is redundant: the build script should trust the SDK is available.

I think the conversation should continue there, given that's the PR that is editing the script.

I also like the idea of having a list of valid numbers to pick from, given it's published at https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022 but I think that would be a good additional option, while we should still default to installing the latest valid version for the user ourselves.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe if we end up implementing the behavior of defaulting to install the latest valid version, we could allow latest to be a special valid content for .windows-10-sdk-version, that way if the file is not present it won't install the SDK at all, but if it's present with latest it will install the latest (and if it's present with a specific version it will deterministically install that specific version, acting as a lockfile) 🤔

That way the implicit auto-install from prepare_windows_host_for_app_distibution if file is present would still work for that default-version fallback… 🤔


Write-Output "Visual Studio Build Tools + Windows 10 ($windows10SDKVersion) SDK installation completed. SDK path: $sdkPath."
Write-Output "Windows 10 SDK path: $sdkPath."

Write-Output "~~~ Cleaning up..."
Remove-Item -Path $buildToolsPath
Write-Output "All cleaned up."
99 changes: 99 additions & 0 deletions bin/prepare_windows_host_for_app_distribution.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Stop script execution when a non-terminating error occurs
$ErrorActionPreference = "Stop"

Write-Host "--- :windows: Setting up Windows for app distribution"

Write-Host "Current working directory: $PWD"

Write-Host "Enable long path behavior"
# See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1

# Disable Windows Defender before starting – otherwise our performance is terrible
Write-Host "Disable Windows Defender..."
$avPreference = @(
@{DisableArchiveScanning = $true}
@{DisableAutoExclusions = $true}
@{DisableBehaviorMonitoring = $true}
@{DisableBlockAtFirstSeen = $true}
@{DisableCatchupFullScan = $true}
@{DisableCatchupQuickScan = $true}
@{DisableIntrusionPreventionSystem = $true}
@{DisableIOAVProtection = $true}
@{DisablePrivacyMode = $true}
@{DisableScanningNetworkFiles = $true}
@{DisableScriptScanning = $true}
@{MAPSReporting = 0}
@{PUAProtection = 0}
@{SignatureDisableUpdateOnStartupWithoutEngine = $true}
@{SubmitSamplesConsent = 2}
@{ScanAvgCPULoadFactor = 5; ExclusionPath = @("D:\", "C:\")}
@{DisableRealtimeMonitoring = $true}
@{ScanScheduleDay = 8}
)

$avPreference += @(
@{EnableControlledFolderAccess = "Disable"}
@{EnableNetworkProtection = "Disabled"}
)

$avPreference | Foreach-Object {
$avParams = $_
Set-MpPreference @avParams
}

# https://github.com/actions/runner-images/issues/4277
# https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-compatibility?view=o365-worldwide
$atpRegPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection'
if (Test-Path $atpRegPath) {
Write-Host "Set Microsoft Defender Antivirus to passive mode"
Set-ItemProperty -Path $atpRegPath -Name 'ForceDefenderPassiveMode' -Value '1' -Type 'DWORD'
}

# From https://stackoverflow.com/a/46760714
Write-Host "--- :windows: Setting up Package Manager"
$env:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"

# This should avoid issues with symlinks not being supported in Windows.
#
# See how this build failed
# https://buildkite.com/automattic/beeper-desktop/builds/2895#01919738-7c6e-4b82-8d1d-1c1800481740
Write-Host "--- :windows: :linux: Enable developer mode to use symlinks"

$developerMode = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

if ($developerMode.State -eq 'Enabled') {
Write-Host "Developer Mode is already enabled."
} else {
Write-Host "Enabling Developer Mode..."
try {
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart
} catch {
Write-Host "Failed to enable Developer Mode. Continuing without it..."
}
}

Write-Host "--- :lock_with_ink_pen: Download Code Signing Certificate"
$certificateBinPath = "certificate.bin"
$EncodedText = aws secretsmanager get-secret-value --secret-id windows-code-signing-certificate `
| jq -r '.SecretString' `
| Out-File $certificateBinPath
$certificatePfxPath = "certificate.pfx"
certutil -decode $certificateBinPath $certificatePfxPath
Write-Host "Code signing certificate downloaded at: $((Get-Item $certificatePfxPath).FullName)"

Write-Host "--- :windows: Checking whether to install Windows 10 SDK..."

# When using Electron Forge and electron2appx, building Appx requires the Windows 10 SDK
#
# See https://github.com/hermit99/electron-windows-store/tree/v2.1.2?tab=readme-ov-file#usage

$windowsSDKVersionFile = ".windows-10-sdk-version"
if (Test-Path $windowsSDKVersionFile) {
Write-Host "Found $windowsSDKVersionFile file, installing Windows 10 SDK..."
& "$PSScriptRoot\install_windows_10_sdk.ps1"
If ($LastExitCode -ne 0) { Exit $LastExitCode }
} else {
Write-Host "No $windowsSDKVersionFile file found, skipping Windows 10 SDK installation."
}
Copy link
Contributor Author

@mokagio mokagio Feb 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example of this finding the SDK file

image

Example of this not finding the SDK file

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that I'm unsure of is whether we should provide a way to bypass or modify this behavior.

Example:

  • Something might be broken in the plugin and a client might need to install the SDK manually; or
  • For whichever reason, it's not possible to use the default file name for the SDK version

I don't think we need to stress much about implementing this flexibility from the get go, so if you agree I'd leave it as a followup, but the code feels incomplete or suboptimal without it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

I wouldn't mind adding an escape hatch, that might be a good idea indeed (though one could argue that'd be YAGNI…)

If it's easy enough to implement and doesn't complicate the script logic too much (which I wouldn't expect to), I'd say go for it.

134 changes: 5 additions & 129 deletions bin/prepare_windows_host_for_node.ps1
Original file line number Diff line number Diff line change
@@ -1,134 +1,10 @@
# Stop script execution when a non-terminating error occurs
$ErrorActionPreference = "Stop"

if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) {
Write-Host "--- :bug: Running as Administrator"
} else {
Write-Host "--- :bug: Running as not Administrator"
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$roles = $principal.Identity.Groups | ForEach-Object {
$_.Translate([Security.Principal.NTAccount]).Value
}
Write-Host "Your roles are:"
$roles | ForEach-Object { Write-Host " - $_" }
}
$newScript = "prepare_windows_host_for_app_distribution.ps1"

Write-Host "--- :windows: Setting up Windows for Node and Electron builds"
Write-Host "+++ :warning: This command is deprecated"
Write-Host "Please use $newScript instead"
Write-Host "Now calling $newScript..."

Write-Host "Enable long path behavior"
# See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1

# Disable Windows Defender before starting – otherwise our performance is terrible
Write-Host "Disable Windows Defender..."
$avPreference = @(
@{DisableArchiveScanning = $true}
@{DisableAutoExclusions = $true}
@{DisableBehaviorMonitoring = $true}
@{DisableBlockAtFirstSeen = $true}
@{DisableCatchupFullScan = $true}
@{DisableCatchupQuickScan = $true}
@{DisableIntrusionPreventionSystem = $true}
@{DisableIOAVProtection = $true}
@{DisablePrivacyMode = $true}
@{DisableScanningNetworkFiles = $true}
@{DisableScriptScanning = $true}
@{MAPSReporting = 0}
@{PUAProtection = 0}
@{SignatureDisableUpdateOnStartupWithoutEngine = $true}
@{SubmitSamplesConsent = 2}
@{ScanAvgCPULoadFactor = 5; ExclusionPath = @("D:\", "C:\")}
@{DisableRealtimeMonitoring = $true}
@{ScanScheduleDay = 8}
)

$avPreference += @(
@{EnableControlledFolderAccess = "Disable"}
@{EnableNetworkProtection = "Disabled"}
)

$avPreference | Foreach-Object {
$avParams = $_
Set-MpPreference @avParams
}

# https://github.com/actions/runner-images/issues/4277
# https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-compatibility?view=o365-worldwide
$atpRegPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection'
if (Test-Path $atpRegPath) {
Write-Host "Set Microsoft Defender Antivirus to passive mode"
Set-ItemProperty -Path $atpRegPath -Name 'ForceDefenderPassiveMode' -Value '1' -Type 'DWORD'
}

Write-Host "--- :lock_with_ink_pen: Downloading Code Signing Certificate"
$EncodedText = aws secretsmanager get-secret-value --secret-id windows-code-signing-certificate | jq -r '.SecretString' | Out-File 'certificate.bin'
certutil -decode certificate.bin certificate.pfx
If ($LastExitCode -ne 0) { Exit $LastExitCode }

# From https://stackoverflow.com/a/46760714
Write-Host "--- :windows: Setting up Package Manager"
$env:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"

# This should avoid issues with symlinks not being supported in Windows.
#
# See how this build failed
# https://buildkite.com/automattic/beeper-desktop/builds/2895#01919738-7c6e-4b82-8d1d-1c1800481740
Write-Host "--- :windows: :linux: Enable developer mode to use symlinks"

$developerMode = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

if ($developerMode.State -eq 'Enabled') {
Write-Host "Developer Mode is already enabled."
} else {
Write-Host "Enabling Developer Mode..."
try {
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart
} catch {
Write-Host "Failed to enable Developer Mode. Continuing without it..."
}
}

Write-Host "--- :node: Installing NVM"
choco install nvm.portable -y
If ($LastExitCode -ne 0) { Exit $LastExitCode }

Write-Host "--- :hammer: Custom PATH refresh post NVM installation to avoid losing previous PATH changes"
Write-Host "PATH before refreshenv is $env:PATH"
# It looks like out of the box, calling refreshenv at this point erases various PATH modifications made by the rest of our automation.
#
# See https://buildkite.com/automattic/beeper-desktop/builds/2893#01919717-d0d0-441d-a85d-0fe3223467d2/195
#
# To avoid the issue, we save the PATH pre-refreshenv and then manually add all the components that were removed.
$originalPath = "$env:PATH"
refreshenv
$mergedPath = "$env:PATH;$originalPath" -split ";" | Select-Object -Unique -Skip 1
$env:PATH = ($mergedPath -join ";")
Write-Host "PATH after refreshenv is $env:PATH"

$nvmRCPath = '.nvmrc'
if (-not (Test-Path $nvmRCPath)) {
Write-Host "No .nvmrc found. Skipping Node set up."
Exit 0
}

Write-Host "--- :node: Installing Node"
$nvmVersion=(Get-Content -Path $nvmRCPath -Total 1)
Write-Host "Switching to nvm version defined in .nvmrc: $nvmVersion"

nvm install $nvmVersion
nvm use $nvmVersion
If ($LastExitCode -ne 0) { Exit $LastExitCode }

Write-Host "--- :hammer: Custom PATH refresh post NVM installation to avoid losing previous PATH changes"
Write-Host "PATH before refreshenv is $env:PATH"
# It looks like out of the box, calling refreshenv at this point erases various PATH modifications made by the rest of our automation.
#
# See https://buildkite.com/automattic/beeper-desktop/builds/2893#01919717-d0d0-441d-a85d-0fe3223467d2/195
#
# To avoid the issue, we save the PATH pre-refreshenv and then manually add all the components that were removed.
$originalPath = "$env:PATH"
refreshenv
$mergedPath = "$env:PATH;$originalPath" -split ";" | Select-Object -Unique -Skip 1
$env:PATH = ($mergedPath -join ";")
Write-Host "PATH after refreshenv is $env:PATH"
& "$PSScriptRoot\$newScript"