wintriallab/scripts/trial-iso-updater.ps1

180 lines
8.1 KiB
PowerShell
Raw Normal View History

<#
This script is part of my ISO updater workflow.
It expects to be run on a machine that has applied Windows updates, but not deleted its cache directory at `${env:WinDir}\SoftwareDistribution\Download`.
It downloads the Windows trial ISO that corresponds to the machine that runs it and applies all Windows Updates to that ISO that are downloaded to its cache directory. Note that only MSI updates can be applied this way. The vast majority of updates are MSI updates, but other types of updates such as EXE updates cannot be applied to an ISO.
#>
[cmdletbinding()] param(
$VagrantSharePath = "C:\Vagrant"
)
import-module $PSScriptRoot\wintriallab-postinstall.psm1
$errorActionPreference = "Stop"
<#
.description
Get the path of the Windows ADK or AIK or whatever the fuck they're calling it from a format string
- {0} is always the WAIK directory
- e.g. "C:\Program Files (x86)\Windows Kits\8.1\"
- e.g. "X:\Program Files\Windows Kits\8.0"
- {1} is always the host architecture (x86 or amd64)
- i THINK this is right, but I don't understand WHY. why do you need an amd64 version of oscdimg.exe?
- however, there are arm executables lying around, and i definitely can't execute those. wtf?
So we expect a string like "{0}\bin\{1}\wsutil.exe"
#>
function Get-AdkPath {
[cmdletbinding()] param(
[parameter(mandatory=$true)] [string] $pathFormatString
)
$adkPath = ""
$possibleAdkPaths = @("${env:ProgramFiles(x86)}\Windows Kits\8.1","${env:ProgramFiles}\Windows Kits\8.1")
$possibleAdkPaths |% { if (test-path $_) { $adkPath = $_ } }
if (-not $adkPath) { throw "Could not find the Windows Automated Installation Kit" }
Write-EventLogWrapper "Found the WAIK at '$adkPath'"
$arch = Get-OSArchitecture
$resolvedPath = $null
if ($arch -match $ArchitectureId.i386) {
$formatted = $pathFormatString -f $adkPath,$arch
if (test-path $formatted) { $resolvedPath = $formatted }
}
elseif ($arch -match $ArchitectureId.amd64) {
foreach ($goddammit in @("amd64","x64")) {
$formatted = $pathFormatString -f $adkPath,$goddammit
if (test-path $formatted) { $resolvedPath = $formatted }
}
}
if ($resolvedPath) {
Write-EventLogWrapper "Resolved ADK path to '$resolvedPath'"
return $resolvedPath
}
else {
$message = "Could not resolve format string '$pathFormatString' to an existing path"
Write-EventLogWrapper $message
throw $message
}
}
<#
.synopsis
Create a new Windows ISO from a working Windows ISO + a new install.wim file
.parameter SourceIsoPath
Path of a working ISO
.parameter InstallMediaTemp
A temporary directory. NOTE: THE CONTENTS OF THIS DIRECTORY WILL BE ERASED
.parameter InstallWimPath
A new install.wim to use
.parameter OutputIsoPath
Path of the resulting ISO
#>
function New-WindowsInstallMedia {
[cmdletbinding()] param(
[parameter(mandatory=$true)] [string] $sourceIsoPath,
[parameter(mandatory=$true)] [string] $installMediaTemp,
[parameter(mandatory=$true)] [string] $installWimPath,
[parameter(mandatory=$true)] [string] $outputIsoPath
)
$oscdImgPath = Get-AdkPath "{0}\Assessment and Deployment Kit\Deployment Tools\{1}\Oscdimg\oscdimg.exe"
$installWimPath = resolve-path $installWimPath | select -expand path
$installMediaTemp = mkdir -force $installMediaTemp | select -expand fullname
$outputIsoParentPath = split-path $outputIsoPath -parent
$outputIsoFilename = split-path $outputIsoPath -leaf
$outputIsoParentPath = mkdir -force $outputIsoParentPath | select -expand fullname
if (test-path $installMediaTemp) { rm -recurse -force $installMediaTemp }
mkdir -force $installMediaTemp | out-null
$diskVol = get-diskimage -imagepath $sourceIsoPath | get-volume
if (-not $diskVol) {
mount-diskimage -imagepath $sourceIsoPath
$diskVol = get-diskimage -imagepath $sourceIsoPath | get-volume
}
$driveLetter = $diskVol | select -expand DriveLetter
$existingInstallMediaDir = "${driveLetter}:"
# TODO: the first copy here copies the original install.wim, and the second copies the new one over it
# this is really fucking dumb right? but then, THIS is way fucking dumber:
# http://stackoverflow.com/questions/731752/exclude-list-in-powershell-copy-item-does-not-appear-to-be-working
# PS none of those solutions are generic enough to get included so fuck it
copy-item -recurse -path "$existingInstallMediaDir\*" -destination "$installMediaTemp" -verbose:$verbose
remove-item -force -path "$installMediaTemp\sources\install.wim"
copy-item -path $installWimPath -destination "$installMediaTemp\sources\install.wim" -force -verbose:$verbose
$etfsBoot = resolve-path "$existingInstallMediaDir\boot\etfsboot.com" | select -expand Path
$oscdimgCall = '& "{0}" -m -n -b"{1}" "{2}" "{3}"' -f @($oscdImgPath, $etfsBoot, $installMediaTemp, $outputIsoPath)
Write-EventLogWrapper "Calling OSCDIMG: '$oscdimgCall'"
Invoke-ExpressionAndCheck $oscdimgCall -verbose:$verbose
dismount-diskimage -imagepath $sourceIsoPath
}
<#
.synopsis
Download the Windows trial ISO that corresponds to the host OS, copy its install.wim, apply Windows Updates to the copy from the local WSUS cache, and create a new ISO containing the updated install.wim
.notes
TODO: could use some cleanup
TODO: improve documentation of parameters
#>
function Apply-WindowsUpdatesToTrialIso {
[cmdletbinding(DefaultParameterSetName="CorrespondingVersion")]
param(
$WorkingDirectory = (New-TemporaryDirectory | Select -Expand FullName),
[Parameter(Mandatory=$True, ParameterSetName="CorrespondingVersion")] [switch] $CorrespondingVersion,
[Parameter(Mandatory=$True, ParameterSetName="SpecifiedVersion")] $TrialIsoInfo,
[Parameter(Mandatory=$True, ParameterSetName="SpecifiedVersion")] $Architecture,
[Parameter(Mandatory=$True, ParameterSetName="SpecifiedVersion")] $WindowsUpdateCacheDir,
$updatedIsoPath = $null
)
if ($PsCmdlet.ParameterSetName -match "$CorrespondingVersion") {
$TrialIsoInfo = Get-WindowsTrialISO
$Architecture = Get-OSArchitecture
$WindowsUpdateCacheDir = "${env:WinDir}\SoftwareDistribution\Download"
}
$WorkingDirectory = mkdir -Force $WorkingDirectory | Select -Expand FullName
$hostWinVer = [Environment]::OSVersion.Version
$filenameVersionStamp = "$($hostWinVer.Major)-$($hostWinVer.Minor)-$Architecture"
$dateStamp = Get-Date -Format get-date -format yyyy-MM-dd
$pristineIsoPath = "$WorkingDirectory\Windows-$filenameVersionStamp-Pristine.iso"
if (-not $updatedIsoPath) {
$updatedIsoPath = "$WorkingDirectory\Windows-$filenameVersionStamp-Updated-$dateStamp.iso"
}
$installWimFilePath = "$WorkingDirectory\install.wim"
$installWimMountPath = "$WorkingDirectory\mnt"
$nwimTemp = "$WorkingDirectory\NewWindowsInstallMediaTemp"
Get-WebUrl -url $TrialIsoInfo.URL -outFile $pristineIsoPath
$mountedIso = Mount-DiskImage -ImagePath $pristineIsoPath -PassThru
cp "$($mountedIso.DevicePath)\sources\install.wim" $installWimFilePath
Set-ItemProperty -Path $installWimFilePath -Name IsReadOnly -Value $false -Force
Dismount-DiskImage -ImagePath $pristineIsoPath
foreach ($wimInfo in (Get-WindowsImage -ImagePath $installWimFilePath)) {
$wimMountSubdir = mkdir "${installWimMountPath}\$(wimInfo.ImageIndex)" -force | Select -Expand FullName
Mount-WindowsImage -ImagePath $installWimFilePath -Index $wimInfo.ImageIndex -Path $wimMountSubdir
try {
Add-WindowsPackage -PackagePath $WindowsUpdateCacheDir -Path $wimMountSubdir
}
catch {
Write-EventLogWrapper "Caught error(s) when installing packages:`n`n$_`n"
}
Dismount-WindowsImage -Path $wimMountSubdir -Save
}
New-WindowsInstallMedia -SourceIsoPath $pristineIsoPath -InstallMediaTemp $nwimTemp -InstallWimPath $installWimFilePath -OutputIsoPath $updatedIsoPath
Get-Item $updatedIsoPath
}
$iso = Apply-WindowsUpdatesToTrialIso -CorrespondingVersion
mv $iso "$VagrantSharePath\"
Stop-Computer