Skip to content

Add one-command Windows ACPI E2E #60

Add one-command Windows ACPI E2E

Add one-command Windows ACPI E2E #60

Workflow file for this run

# Builds the ODP Validation OS VHDX image on a Windows runner.
#
# SPDX-License-Identifier: MIT
#
# Downloads pre-built driver assets from the latest odp-windows-drivers release,
# unzips them, builds a Validation OS WIM, injects the QEMU drivers, and converts
# the result to VHDX. The list of driver assets to download is maintained in
# postbuild/os/prebuilt/driverlist.txt.
#
# It also downloads the EC test apps from the latest odp-platform-common release,
# extracts them to C:\ectest, and matches the payload's `acpi` source contract.
#
# Pull requests build the image for validation only. Every other trigger publishes
# the stable base used by `make run_os` and `make windows-acpi-e2e`.
#
# Third-party actions are pinned by commit SHA.
#
name: build_os_image
permissions:
contents: read
actions: read
on:
pull_request: &os-image-filters
branches: [main]
paths:
- 'postbuild/os/**'
- '.github/workflows/build-os.yml'
push: *os-image-filters
workflow_dispatch:
inputs:
drivers_repo:
description: 'GitHub repository to download driver assets from (owner/repo)'
required: false
default: 'OpenDevicePartnership/odp-windows-drivers'
type: string
apps_repo:
description: 'GitHub repository to download EC test app assets from (owner/repo)'
required: false
default: 'OpenDevicePartnership/odp-platform-common'
type: string
workflow_call:
inputs:
git_ref:
description: 'Git tag in vYYYY.MM.DD format'
required: true
type: string
drivers_repo:
description: 'GitHub repository to download driver assets from (owner/repo)'
required: false
default: 'OpenDevicePartnership/odp-windows-drivers'
type: string
apps_repo:
description: 'GitHub repository to download EC test app assets from (owner/repo)'
required: false
default: 'OpenDevicePartnership/odp-platform-common'
type: string
concurrency:
group: build_os_image-${{ github.head_ref || github.ref }}
# Superseded PR builds are disposable; a publishing run cancelled mid-upload
# would leave the 'latest' asset deleted and not replaced.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
runs-on: windows-latest
name: Build QEMU ARM64 OS image
timeout-minutes: 30
permissions:
contents: write
defaults:
run:
shell: pwsh
steps:
- name: Clone Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.git_ref || github.ref }}
- name: Resolve driver assets
run: |
$assetNames = Get-Content -Path 'postbuild\os\prebuilt\driverlist.txt' |
ForEach-Object { $_.Trim() } |
Where-Object { $_ -and -not $_.StartsWith('#') }
if (-not $assetNames) {
throw 'No driver names found in postbuild\os\prebuilt\driverlist.txt'
}
if ($assetNames -notcontains 'driver-ectest_kmdf-ARM64-Release') {
throw 'Stable base requires driver-ectest_kmdf-ARM64-Release'
}
$assetNamesFile = Join-Path $env:RUNNER_TEMP 'asset-names.txt'
$assetNames -join "`n" | Out-File -FilePath $assetNamesFile -Encoding utf8
"DRIVER_ASSET_NAMES_FILE=$assetNamesFile" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Download driver assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if (-not (Test-Path $env:DRIVER_ASSET_NAMES_FILE)) {
throw "Driver asset list '$env:DRIVER_ASSET_NAMES_FILE' does not exist"
}
$assetNames = Get-Content -Path $env:DRIVER_ASSET_NAMES_FILE
$driversRepo = '${{ inputs.drivers_repo || 'OpenDevicePartnership/odp-windows-drivers' }}'
# Drivers are published as zip assets on the rolling 'latest' release
# (unlike workflow artifacts, gh does not auto-extract them), so each
# asset is downloaded and manually unzipped into drivers/<name>.
foreach ($name in $assetNames) {
$destPath = Join-Path 'drivers' $name
New-Item -ItemType Directory -Path $destPath -Force | Out-Null
$assetName = "$name.zip"
$zipPath = Join-Path $env:RUNNER_TEMP $assetName
if (Test-Path $zipPath) { Remove-Item -Path $zipPath -Force }
Write-Host "Downloading asset: $assetName -> $destPath"
gh release download latest --repo $driversRepo --pattern $assetName --dir $env:RUNNER_TEMP --clobber
if ($LASTEXITCODE -ne 0) { throw "Failed to download asset $assetName from the latest release of $driversRepo" }
if (-not (Test-Path $zipPath)) { throw "Downloaded asset '$zipPath' does not exist" }
Expand-Archive -Path $zipPath -DestinationPath $destPath -Force
}
- name: Download EC test apps
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$appsRepo = '${{ inputs.apps_repo || 'OpenDevicePartnership/odp-platform-common' }}'
$assetName = 'ec-test-apps-ARM64.zip'
$zipPath = Join-Path $env:RUNNER_TEMP $assetName
if (Test-Path $zipPath) { Remove-Item -Path $zipPath -Force }
Write-Host "Downloading asset: $assetName from $appsRepo"
gh release download latest --repo $appsRepo --pattern $assetName --dir $env:RUNNER_TEMP --clobber
if ($LASTEXITCODE -ne 0) { throw "Failed to download asset $assetName from the latest release of $appsRepo" }
if (-not (Test-Path $zipPath)) { throw "Downloaded asset '$zipPath' does not exist" }
# The archive contains a top-level ectest\ folder; it is extracted into
# the mounted WIM later so it lands at C:\ectest in the running OS.
"EC_TEST_APPS_ZIP=$zipPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Download Validation OS ISO
run: |
curl.exe --fail --location --retry 5 --retry-all-errors `
--continue-at - --output ValidationOS.iso `
https://aka.ms/DownloadValidationOS_26H1_arm64
if ($LASTEXITCODE -ne 0) {
throw "ValidationOS ISO download failed with exit code $LASTEXITCODE"
}
- name: Build WIM
run: |
$mount = Mount-DiskImage -ImagePath (Resolve-Path ValidationOS.iso).Path -PassThru
$drive = ($mount | Get-Volume).DriveLetter
$layout = Join-Path $env:RUNNER_TEMP 'winvos-package-layout'
try {
if (Test-Path $layout) { Remove-Item -Path $layout -Recurse -Force }
New-Item -ItemType Directory -Path (Join-Path $layout 'cabs') -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $layout 'cabs\Extra') -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $layout 'GenImage') -Force | Out-Null
New-Item -ItemType Junction -Path (Join-Path $layout 'cabs\neutral') `
-Target "${drive}:\cabs\Common\neutral" | Out-Null
New-Item -ItemType Junction -Path (Join-Path $layout 'cabs\en-us') `
-Target "${drive}:\cabs\Common\en-us" | Out-Null
New-Item -ItemType Junction -Path (Join-Path $layout 'cabs\Extra\neutral') `
-Target "${drive}:\cabs\Extra\neutral" | Out-Null
New-Item -ItemType Junction -Path (Join-Path $layout 'cabs\Extra\en-us') `
-Target "${drive}:\cabs\Extra\en-us" | Out-Null
New-Item -ItemType Junction -Path (Join-Path $layout 'GenImage\Configs') `
-Target "${drive}:\GenImage\Configs" | Out-Null
$packagePath = Join-Path $layout 'cabs'
cmd /c "${drive}:\GenImage\GenImage.cmd -PackagesList:postbuild\os\prebuilt\armvirt_config.pkg -PackagePath:$packagePath -ImagePath:${drive}:\ -RegistryImport:postbuild\os\prebuilt\armvirt_policy.reg -OutPath:postbuild\os\build -wim -NoWait"
if ($LASTEXITCODE -ne 0) { throw "GenImage.cmd failed with exit code $LASTEXITCODE" }
$wim = Get-ChildItem -Path 'postbuild\os\build' -Filter *.wim -File | Select-Object -First 1
if (-not $wim) { throw 'GenImage produced no .wim in postbuild\os\build' }
"WIM_PATH=$($wim.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
} finally {
if (Test-Path $layout) { Remove-Item -Path $layout -Recurse -Force }
Dismount-DiskImage -ImagePath $mount.ImagePath
}
- name: Validate ValidationOS build floor
run: |
$minimumBuild = 28000
$image = Get-WindowsImage -ImagePath $env:WIM_PATH -Index 1
$version = [version]$image.Version
if ($version.Build -lt $minimumBuild) {
throw "ValidationOS build $($version.Build) is below required build $minimumBuild"
}
Write-Host "ValidationOS build $($version.Build) satisfies floor $minimumBuild"
- name: Inject QEMU drivers and EC test apps into WIM
run: |
if (-not $env:WIM_PATH) {
throw 'WIM_PATH is not set'
}
if (-not (Test-Path $env:DRIVER_ASSET_NAMES_FILE)) {
throw "Driver asset list '$env:DRIVER_ASSET_NAMES_FILE' does not exist"
}
if (-not $env:EC_TEST_APPS_ZIP) {
throw 'EC_TEST_APPS_ZIP is not set'
}
$wimPath = Resolve-Path $env:WIM_PATH
$mountPath = Join-Path $env:RUNNER_TEMP 'os-image-mount'
if (Test-Path $mountPath) {
Remove-Item -Path $mountPath -Recurse -Force
}
New-Item -ItemType Directory -Path $mountPath -Force | Out-Null
$assetNames = Get-Content -Path $env:DRIVER_ASSET_NAMES_FILE
$driverRoots = $assetNames | ForEach-Object { Resolve-Path (Join-Path 'drivers' $_) }
if (-not $driverRoots) {
throw "No driver paths resolved from $env:DRIVER_ASSET_NAMES_FILE"
}
$infPaths = foreach ($driverRoot in $driverRoots) {
Get-ChildItem -Path $driverRoot -Recurse -Filter *.inf -File
}
if (-not $infPaths) {
throw "No INF files found for any driver listed in $env:DRIVER_ASSET_NAMES_FILE"
}
$infPaths = $infPaths | Sort-Object FullName -Unique
try {
dism.exe /Mount-Image /ImageFile:$wimPath /Index:1 /MountDir:$mountPath
if ($LASTEXITCODE -ne 0) { throw "Failed to mount WIM with exit code $LASTEXITCODE" }
foreach ($infPath in $infPaths) {
Write-Host "Injecting driver $($infPath.FullName)"
dism.exe /Image:$mountPath /Add-Driver /Driver:$($infPath.FullName)
if ($LASTEXITCODE -ne 0) { throw "Failed to add driver $($infPath.FullName) with exit code $LASTEXITCODE" }
}
# ectest must go inside the WIM (not onto the applied partition):
# Validation OS RAM-boots the WIM, so partition-only files are not
# visible at runtime. Drivers already work for this reason.
# The archive contains a top-level ectest\ folder, so extracting it
# at the WIM root produces \ectest (C:\ectest once booted).
Expand-Archive -Path $env:EC_TEST_APPS_ZIP -DestinationPath $mountPath -Force
$ecTestDir = Join-Path $mountPath 'ectest'
$expectedApps = 'ec-test-tui.exe', 'ec-test-cli.exe'
foreach ($app in $expectedApps) {
$appPath = Join-Path $ecTestDir $app
if (-not (Test-Path $appPath)) {
throw "Expected EC test app missing: $appPath"
}
}
Write-Host "Extracted EC test apps into WIM at $ecTestDir"
dism.exe /Unmount-Image /MountDir:$mountPath /Commit
if ($LASTEXITCODE -ne 0) { throw "Failed to commit WIM changes with exit code $LASTEXITCODE" }
} finally {
$mountedImage = Get-WindowsImage -Mounted | Where-Object { $_.Path -eq $mountPath }
if ($mountedImage) {
dism.exe /Unmount-Image /MountDir:$mountPath /Discard | Out-Null
}
if (Test-Path $mountPath) {
Remove-Item -Path $mountPath -Recurse -Force -ErrorAction SilentlyContinue
}
}
- name: Convert WIM to VHDX
run: |
if (-not $env:WIM_PATH) {
throw 'WIM_PATH is not set'
}
$wimPath = Resolve-Path $env:WIM_PATH
$vhdxPath = Join-Path (Get-Location) 'staging\os-image.vhdx'
$vhdxSizeBytes = 2GB
New-Item -ItemType Directory -Force -Path staging | Out-Null
$vhdx = New-VHD -Path $vhdxPath -SizeBytes $vhdxSizeBytes -Dynamic
$disk = Mount-VHD -Path $vhdxPath -Passthru
try {
$disk | Initialize-Disk -PartitionStyle GPT
$efiPart = $disk | New-Partition -Size 100MB -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' -AssignDriveLetter
$efiPart | Format-Volume -FileSystem FAT32 -NewFileSystemLabel 'EFI' -Confirm:$false | Out-Null
$efiLetter = $efiPart.DriveLetter
$osPart = $disk | New-Partition -UseMaximumSize -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' -AssignDriveLetter
$osPart | Format-Volume -FileSystem NTFS -NewFileSystemLabel 'ValidationOS' -Confirm:$false | Out-Null
$osLetter = $osPart.DriveLetter
Write-Host "EFI partition: ${efiLetter}: OS partition: ${osLetter}:"
Write-Host "Applying WIM to ${osLetter}:\"
dism.exe /Apply-Image /ImageFile:"$wimPath" /Index:1 /ApplyDir:"${osLetter}:\"
if ($LASTEXITCODE -ne 0) { throw "Failed to apply WIM with exit code $LASTEXITCODE" }
Write-Host "Creating boot files on ${efiLetter}:\"
bcdboot "${osLetter}:\Windows" /s "${efiLetter}:" /f UEFI
if ($LASTEXITCODE -ne 0) { throw "bcdboot failed with exit code $LASTEXITCODE" }
Write-Host "Enabling testsigning"
bcdedit /store ${efiLetter}:\EFI\Microsoft\Boot\BCD /set "{default}" testsigning on
if ($LASTEXITCODE -ne 0) { throw "Enabling testsigning failed with exit code $LASTEXITCODE" }
Write-Host "Enabling debug"
bcdedit /store ${efiLetter}:\EFI\Microsoft\Boot\BCD /set "{default}" debug on
if ($LASTEXITCODE -ne 0) { throw "Enabling debug failed with exit code $LASTEXITCODE" }
} finally {
Dismount-VHD -Path $vhdxPath
}
Write-Host "VHDX created: $vhdxPath ($('{0:N1} MB' -f ((Get-Item $vhdxPath).Length / 1MB)))"
- name: Package and publish image
# Pushes only ever fire on main; PRs are the only trigger that must not publish.
if: github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$vhdxPath = 'staging\os-image.vhdx'
$zipPath = Join-Path $env:RUNNER_TEMP 'os-image.zip'
if (Test-Path $zipPath) { Remove-Item -Path $zipPath -Force }
# Relies on the job's pwsh shell: Windows PowerShell 5.1 cannot write the
# ZIP64 entries a VHDX this size needs.
Compress-Archive -Path $vhdxPath -DestinationPath $zipPath
Write-Host "Packaged: $zipPath ($('{0:N1} MB' -f ((Get-Item $zipPath).Length / 1MB)))"
# Uploading only the asset leaves the hand-created 'latest' prerelease and
# its tag untouched; --clobber replaces the asset from the previous run.
gh release upload latest $zipPath --repo $env:GITHUB_REPOSITORY --clobber
if ($LASTEXITCODE -ne 0) { throw 'Failed to upload os-image.zip to the latest release.' }