Skip to content

Add reusable action for MSVC + Win SDK overrides #956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
236 changes: 236 additions & 0 deletions .github/actions/setup-build/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
name: Setup build
description: Sets up the build environment for the current job

inputs:
windows-sdk-version:
Copy link
Owner

Choose a reason for hiding this comment

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

Any reason to prefix the options with windows-?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is going to be expanded to also add sccache and cmake, which are cross-platforms. I intend to reduce the number of if: in the swift workflow.
However, I did rename the msvc-version input, since Windows is implied there.

description: The Windows SDK version to use, e.g. "10.0.22621.0"
required: false
type: string
msvc-version:
description: The Windows MSVC version to use, e.g. "14.42"
required: false
type: string
setup-vs-dev-env:
Copy link
Owner

Choose a reason for hiding this comment

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

Especially since this is also Windows specific

description: Whether to set up a Visual Studio Dev Environment
default: false
required: false
type: boolean
target-arch:
description: The target architecture, "x86", "amd64" or "arm64". Defaults to the host architecture.
required: false
type: string

runs:
using: composite
steps:
- name: Verify input
id: verify-input
shell: pwsh
run: |
if ($IsWindows) {
Copy link
Owner

Choose a reason for hiding this comment

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

Where is $IsWindows defined?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's an automatic variable set up by pwsh. I was looking for something better to use to differentiate between Windows/macOS/Other. Unfortunately, the only other option I found is [environment]::OSVersion.Platform, which is set to Unix on MacOS.

$HostOS = "windows"
} elseif ($IsMacOS) {
$HostOS = "mac"
} else {
Write-Output "::error::Unsupported host OS."
exit 1
}

$Arch = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString()
switch ($Arch) {
"X64" { $HostArch = "amd64" }
"Arm64" { $HostArch = "arm64" }
default {
Write-Output "::error::Unsupported host architecture: `"$HostArch`""
exit 1
}
}

# Validate the MSVC version input.
# If specified, it is expected to have a format "major.minor", without the build and
# revision numbers. When a value such as "14.42" is parsed as a `System.Version`, the build
# and revision numbers in that object are set to -1.
$MSVCVersion = "${{ inputs.msvc-version }}"
if ($MSVCVersion -ne "") {
$ParsedMSVCVersion = [System.Version]::Parse($MSVCVersion)
if ($ParsedMSVCVersion -eq $null) {
Write-Output "::error::Invalid Windows MSVC version: `"${MSVCVersion}`"."
exit 1
}
if ($ParsedMSVCVersion.Major -ne 14) {
Write-Output "::error::Unsupported Windows MSVC version (major version not supported): `"${MSVCVersion}`"."
exit 1
}
if ($ParsedMSVCVersion.Build -ne -1) {
Write-Output "::error::Unsupported Windows MSVC version (build version was specified): `"${MSVCVersion}`"."
exit 1
}
if ($ParsedMSVCVersion.Revision -ne -1) {
Write-Output "::error::Unsupported Windows MSVC version (revision version was specified): `"${MSVCVersion}`"."
exit 1
}
}

switch ("${{ inputs.target-arch }}") {
"x86" { $TargetArch = "x86" }
"amd64" { $TargetArch = "amd64" }
"arm64" { $TargetArch = "arm64" }
"" { $TargetArch = $HostArch }
default {
Write-Output "::error::Unsupported target architecture: `"${{ inputs.target-arch }}`""
exit 1
}
}

Write-Output "ℹ️ Host OS: $HostOS"
Write-Output "ℹ️ Host architecture: $HostArch"
Write-Output "ℹ️ Host OS: $HostOS"
Write-Output "ℹ️ Host architecture: $TargetArch"

@"
host-os=$HostOS
host-arch=$HostArch
target-arch=$TargetArch
"@ | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append

- name: Install Windows SDK version ${{ inputs.windows-sdk-version }}
if: steps.verify-input.outputs.host-os == 'windows' && inputs.windows-sdk-version != ''
shell: pwsh
run: |
$WinSdkVersionString = "${{ inputs.windows-sdk-version }}"
$WinSdkVersion = [System.Version]::Parse($WinSdkVersionString)
$WinSdkVersionBuild = $WinSdkVersion.Build

$Win10SdkRoot = Get-ItemPropertyValue `
-Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Kits\Installed Roots" `
-Name "KitsRoot10"
$Win10SdkLib = Join-Path $Win10SdkRoot "Lib"
$Win10SdkInclude = Join-Path $Win10SdkRoot "Include"
$Win10SdkIncludeVersion = Join-Path $Win10SdkInclude $WinSdkVersionString

if (Test-Path -Path $Win10SdkIncludeVersion -PathType Container) {
Write-Output "ℹ️ MSVCPackageVersionWindows SDK ${WinSdkVersionString} already installed."
} else {
# Install the missing SDK.
Write-Output "ℹ️ Installing Windows SDK ${WinSdkVersionString}..."

$InstallerLocation = Join-Path "${env:ProgramFiles(x86)}" "Microsoft Visual Studio" "Installer"
$VSWhere = Join-Path "${InstallerLocation}" "VSWhere.exe"
$VSInstaller = Join-Path "${InstallerLocation}" "vs_installer.exe"
$InstallPath = (& "$VSWhere" -latest -products * -format json | ConvertFrom-Json).installationPath
$process = Start-Process "$VSInstaller" `
-PassThru `
-ArgumentList "modify", `
"--installPath", "`"$InstallPath`"", `
"--channelId", "https://aka.ms/vs/17/release/channel", `
"--quiet", "--norestart", "--nocache", `
"--add", "Microsoft.VisualStudio.Component.Windows11SDK.${WinSdkVersionBuild}"
$process.WaitForExit()

if (Test-Path -Path $Win10SdkIncludeVersion -PathType Container) {
Write-Output "ℹ️ Windows SDK ${WinSdkVersionString} installed successfully."
} else {
Write-Output "::error::Failed to install Windows SDK ${WinSdkVersionString}."
Write-Output "Installer log:"
$log = Get-ChildItem "${env:TEMP}" -Filter "dd_installer_*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
Get-Content $log.FullName
exit 1
}
}

# Remove more recent Windows SDKs, if present. This is used to work
# around issues where LLVM uses the most recent Windows SDK.
# This should be removed once a more permanent solution is found.
# See https://github.com/compnerd/swift-build/issues/958 for details.
Get-ChildItem -Path $Win10SdkInclude -Directory | ForEach-Object {
$IncludeDirName = $_.Name
try {
$IncludeDirVersion = [System.Version]::Parse($IncludeDirName)
if ($IncludeDirVersion -gt $WinSdkVersion) {
$LibDirVersion = Join-Path $Win10SdkLib $IncludeDirName
Write-Output "ℹ️ Removing folders for Windows SDK ${IncludeDirVersion}."
Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction Ignore
Remove-Item -Path $LibDirVersion -Recurse -Force -ErrorAction Ignore
}
} catch {
# Skip if the directory cannot be parsed as a version.
}
}

- name: Install Windows MSVC version ${{ inputs.msvc-version }}
if: steps.verify-input.outputs.host-os == 'windows' && inputs.msvc-version != ''
shell: pwsh
run: |
# This is assuming a VS2022 toolchain. e.g.
# MSVC 14.42 corresponds to the 14.42.17.12 package.
# MSVC 14.43 corresponds to the 14.43.17.13 package.
$MSVCVersionString = "${{ inputs.msvc-version }}"

$InstallerLocation = Join-Path "${env:ProgramFiles(x86)}" "Microsoft Visual Studio" "Installer"
$VSWhere = Join-Path "${InstallerLocation}" "VSWhere.exe"
$VSInstaller = Join-Path "${InstallerLocation}" "vs_installer.exe"
$InstallPath = (& "$VSWhere" -latest -products * -format json | ConvertFrom-Json).installationPath
$MSVCDir = Join-Path $InstallPath "VC" "Tools" "MSVC"

# Check if this MSVC version is already installed.
Get-ChildItem -Path $MSVCDir -Directory | ForEach-Object {
$MSVCDirName = $_.Name
if ($MSVCDirName.StartsWith($MSVCVersionString)) {
Write-Output "ℹ️ MSVCPackageVersionMSVC ${MSVCVersionString} already installed."
exit 0
}
}

# Compute the MSVC version package name from the MSVC version, assuming this is coming from
# a VS2022 installation. The version package follows the following format:
# * Major and minor version are the same as the MSVC version.
# * Build version is always 17 (VS2002 is VS17).
# * The revision is set to the number of minor versions since VS17 release.
$MSVCVersion = [System.Version]::Parse($MSVCVersionString)
$MajorVersion = $MSVCVersion.Major
$MinorVersion = $MSVCVersion.Minor
$BuildVersion = 17
Copy link
Owner

Choose a reason for hiding this comment

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

Why hardcoded to VS17? (2022)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am trying to avoid having to specify 2 versions: the MSVC build tools version and the package version, so I am computing the package version from the major.minor build tools version.
To simplify things, I am only supporting VS17 here. I do not expect that we also want to support VS16 toolchains, but this will require updating once Microsoft releases VS18.

$RevisionVersion = $MinorVersion - 30
Copy link
Owner

Choose a reason for hiding this comment

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

What is this based on?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

14.29.16.11 was the last VS16 toolchain version.
14.30.17.0 was the first VS17 toolchain version.
14.31.17.1 was the second VS17 toolchain version.
Etc.

Basically, the first toolchain that shipped with VS17 was 14.30 and the revision number is based on that. I added a comment to explain that.

You can find the package names here. Note that counterintuitively, while the build tools version is 14.42, the compiler itself has version 19.42.
Point is, we can't have nice things.

$MSVCPackageVersion = "${MajorVersion}.${MinorVersion}.${BuildVersion}.${RevisionVersion}"

# Install the missing MSVC version.
Write-Output "ℹ️ Installing MSVC packages for ${MSVCPackageVersion}..."
$process = Start-Process "$VSInstaller" `
-PassThru `
-ArgumentList "modify", `
"--installPath", "`"$InstallPath`"", `
"--channelId", "https://aka.ms/vs/17/release/channel", `
"--quiet", "--norestart", "--nocache", `
"--add", "Microsoft.VisualStudio.Component.VC.${MSVCPackageVersion}.x86.x64", `
"--add", "Microsoft.VisualStudio.Component.VC.${MSVCPackageVersion}.ATL", `
"--add", "Microsoft.VisualStudio.Component.VC.${MSVCPackageVersion}.ARM64", `
"--add", "Microsoft.VisualStudio.Component.VC.${MSVCPackageVersion}.ATL.ARM64"
$process.WaitForExit()

# Check if the MSVC version was installed successfully.
$MSVCDirFound = $false
foreach ($dir in Get-ChildItem -Path $MSVCDir -Directory) {
$MSVCDirName = $dir.Name
if ($MSVCDirName.StartsWith($MSVCVersionString)) {
Write-Output "ℹ️ MSVC ${MSVCVersionString} installed successfully."
$MSVCDirFound = $true
break
}
}

if (-not $MSVCDirFound) {
Write-Output "::error::Failed to install MSVC ${MSVCVersionString}."
Write-Output "Installer log:"
$log = Get-ChildItem "${env:TEMP}" -Filter "dd_installer_*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
Get-Content $log.FullName
exit 1
}

- name: Setup Visual Studio Developer Environment
if: steps.verify-input.outputs.host-os == 'windows' && inputs.setup-vs-dev-env
uses: compnerd/gha-setup-vsdevenv@5eb3eae1490d4f7875d574c4973539f69109700d # main
with:
host_arch: ${{ steps.verify-input.outputs.host-arch }}
arch: ${{ steps.verify-input.outputs.target-arch }}
winsdk: ${{ inputs.msvc-version }}
toolset_version: ${{ inputs.msvc-version }}
129 changes: 129 additions & 0 deletions .github/workflows/test-setup-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: Test the setup-build action
on:
pull_request:
branches:
- 'main'
paths:
- '.github/actions/action.yml'
- '.github/workflows/test-setup-build.yml'
workflow_dispatch:
inputs:
windows-runner:
description: "The Windows runner to use"
required: false
type: string
workflow_call:
inputs:
windows-runner:
description: "The Windows runner to use"
required: false
type: string

env:
TEST_WIN_SDK_VERSION: 10.0.22621.0
TEST_MSVC_VERSION: 14.42

jobs:
test-setup-build-windows:
name: Test MSVC and Windows SDK environment setup
runs-on: ${{ inputs.windows-runner || 'windows-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v4.2.2

- name: Set up build
uses: ./.github/actions/setup-build
with:
windows-sdk-version: ${{ env.TEST_WIN_SDK_VERSION }}
msvc-version: ${{ env.TEST_MSVC_VERSION }}
setup-vs-dev-env: true

- name: Check environment
run: |
$HasError = $false

$ParsedWinSdkVersion = [System.Version]::Parse($env:TEST_WIN_SDK_VERSION)
$Win10SdkRoot = Get-ItemPropertyValue `
-Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Kits\Installed Roots" `
-Name "KitsRoot10"
$Win10SdkInclude = Join-Path $Win10SdkRoot "Include"

# Check if the Windows SDK version is installed.
$ExpectedWinSdkDir = Join-Path $Win10SdkInclude "$($env:TEST_WIN_SDK_VERSION)"
if (Test-Path -Path $ExpectedWinSdkDir) {
Write-Output "✅ Windows SDK version `"${env:TEST_WIN_SDK_VERSION}`" is installed."
} else {
Write-Output "::error::Expected Windows SDK version not found: `"${env:TEST_WIN_SDK_VERSION}`"."
$HasError = $true
}

# Check if Windows SDK versions greater than the expected version are installed.
$UnexpectedSdkFound = $false
Get-ChildItem -Path $Win10SdkInclude -Directory | ForEach-Object {
$Version = $_.Name
try {
$ParsedVersion = [System.Version]::Parse($Version)
if ($ParsedVersion -gt $ParsedWinSdkVersion) {
Write-Output "::error::Unexpected Windows SDK version found: `"${Version}`" (greater than expected: `"${env:TEST_WIN_SDK_VERSION}`")."
$HasError = $true
$UnexpectedSdkFound = $true
}
} catch {
# Skip if the directory cannot be parsed as a version.
}
}
if (-not $UnexpectedSdkFound) {
Write-Output "✅ No unexpected Windows SDK versions greater than `"${env:TEST_WIN_SDK_VERSION}`" found."
}

# Check if the correct MSVC version is installed.
$InstallerLocation = Join-Path "${env:ProgramFiles(x86)}" "Microsoft Visual Studio" "Installer"
$VSWhere = Join-Path "${InstallerLocation}" "vswhere.exe"
$InstallPath = (& "$VSWhere" -latest -products * -format json | ConvertFrom-Json).installationPath
$MSVCDir = Join-Path $InstallPath "VC" "Tools" "MSVC"
$DirFound = $false
foreach ($dir in Get-ChildItem -Path $MSVCDir -Directory) {
$MSVCDirName = $dir.Name
if ($MSVCDirName.StartsWith($env:TEST_MSVC_VERSION)) {
$DirFound = $true
break
}
}
if ($DirFound) {
Write-Output "✅ MSVC version `${env:TEST_MSVC_VERSION}`" is installed."
} else {
Write-Output "::error::Expected MSVC version not found: `"${env:TEST_MSVC_VERSION}`"."
$HasError = $true
}

# Check the current cl.exe version by expanding the _MSC_VER macro.
$tempFile = [System.IO.Path]::GetTempFileName().Replace('.tmp', '.c')
Set-Content -Path $tempFile -Value "_MSC_VER"
$clOutput = & cl /nologo /EP $tempFile 2>&1
$lastLine = $clOutput | Select-Object -Last 1
Remove-Item $tempFile -Force

# _MSC_VER expands to a number like 1942 for MSVC 14.42.
$ParsedMSVCVersion = [System.Version]::Parse($env:TEST_MSVC_VERSION)
$ExpectedVersion = ($ParsedMSVCVersion.Major + 5) * 100 + $ParsedMSVCVersion.Minor
if ($lastLine -eq $ExpectedVersion) {
Write-Output "✅ cl.exe reports expected _MSC_VER `"${ExpectedVersion}`"."
} else {
Write-Output "::error::Unexpected MSVC version found: `"${lastLine}`" (expected: `"${ExpectedVersion}`")."
$HasError = $true
}

# Check if the Windows SDK version is set in the environment.
if ($env:UCRTVersion -eq $env:TEST_WIN_SDK_VERSION) {
Write-Output "✅ UCRTVersion environment variable is set to `"${env:TEST_WIN_SDK_VERSION}`"."
} else {
Write-Output "::error::UCRTVersion environment variable (`"${env:UCRTVersion}`") is not set to the expected Windows SDK version (`"${env:TEST_WIN_SDK_VERSION}`")."
$HasError = $true
}

if ($HasError) {
Write-Output "::error::There were errors in the environment setup. Check the logs for details."
exit 1
} else {
Write-Output "🎉 All environment checks passed successfully."
}