Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ _None._

### Breaking Changes

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

### New Features

_None._
- Added new command to install Windows 10 SDK on Windows CI machines, `install_windows_10_sdk.ps1` [#144]
- `prepare_windows_host_for_app_distribution` automatically installs the Windows 10 SDK if version file is found [#144]
- Added new command to run `refreshenv` on Windows preserving the `PATH`, `path_aware_refreshenv` [#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."
25 changes: 25 additions & 0 deletions bin/path_aware_refreshenv.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Wraps Chocolatey's `refreshenv` / `Update-SessionEnvironment` to avoid erasing PATH modifications.
#
# See https://docs.chocolatey.org/en-us/create/cmdlets/update-sessionenvironment/
#
# Use this after installing a package via Chocolatey in a pipeline that modified the PATH at runtime, e.g. after adding a new binary to the PATH.
#
# It seems like calling refreshenv can erase PATH modifications that previous
# steps in an automation script might have made.
#
# See for example the logs in
# 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.

# Stop script execution when a non-terminating error occurs
$ErrorActionPreference = "Stop"

Write-Host "PATH before refreshenv is $env:PATH"
$originalPath = "$env:PATH"
Write-Host "Calling refreshenv..."
refreshenv
$mergedPath = "$env:PATH;$originalPath" -split ";" | Select-Object -Unique -Skip 1
$env:PATH = ($mergedPath -join ";")
Write-Host "PATH after refreshenv is $env:PATH"
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
# Prepares a `windows` CI agent with all the necessary setup so it can build and distribute a windows app
#
# - Enables long path behavior
# - Disable Windows Defender on the CI agent
# - Install the "Chocolatey" package manager
# - Enable dev mode so the agent can support Linux-style symlinks
# - Download Code Signing Certificates(1)
# - Install the Windows 10 SDK if it detected a `.windows-10-sdk-version` file(2)
#
# (1) The certificate it installs is stored in our AWS SecretsManager storage (`windows-code-signing-certificate` secret ID)
# (2) You can skip the Win10 install even if `.windows-10-sdk-version` file is present by using the `SKIP_WINDOWS_10_SDK_INSTALL=1` env var before calling this script
#
# Note: In addition to calling this script, and depending on your client app, you might want to also install `npm` and the `Node.js` packages used by your client app on the agent too. For that part, you should use the `automattic/nvm` Buildkite plugin on the pipeline step's `plugins:` attribute.
#

# 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 " - $_" }
}
Write-Host "--- :windows: Setting up Windows for app distribution"

Write-Host "--- :windows: Setting up Windows for Node and Electron builds"
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
Expand Down Expand Up @@ -60,11 +65,6 @@ if (Test-Path $atpRegPath) {
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)\..\.."
Expand All @@ -89,46 +89,26 @@ if ($developerMode.State -eq 'Enabled') {
}
}

Write-Host "--- :node: Installing NVM"
choco install nvm.portable -y
If ($LastExitCode -ne 0) { Exit $LastExitCode }
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 "--- :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 "--- :windows: Checking whether to install Windows 10 SDK..."

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.
# When using Electron Forge and electron2appx, building Appx requires the Windows 10 SDK
#
# 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"
# 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."
}