Thought I’d share a script I’ve developed for installing local user .MSI apps via Active Setup. This is for per-user application installs and not machine-wide.
# Metascript: Stage installer and write Active Setup key for Spoke Phone
Invoke-ImmyCommand {
# Extract version from MSI file and format for Active Setup (X,X,X,X)
$msiPath = $Using:InstallerFile
$windowsInstaller = New-Object -ComObject WindowsInstaller.Installer
$msiDatabase = $windowsInstaller.OpenDatabase($msiPath, 0)
$view = $msiDatabase.OpenView("SELECT Value FROM Property WHERE Property = 'ProductVersion'")
$view.Execute()
$record = $view.Fetch()
$msiVersion = $record.StringData(1)
# Release COM objects
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($view) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($msiDatabase) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($windowsInstaller) | Out-Null
# Convert dot-separated to comma-separated and ensure 4 parts (e.g. "1.2.3" -> "1,2,3,0")
$versionParts = $msiVersion.Split('.')
while ($versionParts.Count -lt 4) { $versionParts += "0" }
$Version = $versionParts[0..3] -join ","
Write-Host "MSI Product Version: $msiVersion -> Active Setup Version: $Version"
$guid = "{A1B2C3D4-0000-0000-0000-SPOKE00000001}"
$registryBase = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\$guid"
$stagingDir = "C:\ProgramData\ImmyBot\SpokePhone"
$stagedFile = Join-Path $stagingDir (Split-Path $Using:InstallerFile -Leaf)
# Stage installer to a user-readable location
if (-not (Test-Path $stagingDir)) {
New-Item -Path $stagingDir -ItemType Directory -Force | Out-Null
}
Write-Host "Copying installer to staging directory: $stagingDir"
$robocopy = robocopy (Split-Path $Using:InstallerFile -Parent) $stagingDir (Split-Path $Using:InstallerFile -Leaf) /R:3 /W:5
if ($LASTEXITCODE -ge 8) {
Write-Host "Robocopy failed with exit code $LASTEXITCODE"
throw "Failed to stage installer to $stagingDir"
}
Write-Host "Installer staged successfully to: $stagedFile"
# Write Active Setup HKLM key pointing to the staged path
if (-not (Test-Path $registryBase)) {
New-Item -Path $registryBase -Force | Out-Null
}
Set-ItemProperty -Path $registryBase -Name "(Default)" -Value "Spoke Phone"
Set-ItemProperty -Path $registryBase -Name "Version" -Value $Version -Force
Set-ItemProperty -Path $registryBase -Name "StubPath" -Value "msiexec.exe /i `"$stagedFile`" /qn /norestart"
Set-ItemProperty -Path $registryBase -Name "ComponentIDs" -Value "SpokePhone"
Write-Host "Active Setup key written: $registryBase (v$Version)"
}
Needs to be given a parameter value for changing the install parameters in the Stubpath during deployment creation but otherwise works as expected.
The detection script matches installer version against registry Version value, failing if either are missing or mis-match.
# Detection Script: Active Setup Verification
Invoke-ImmyCommand {
$UpgradeCode = $Using:UpgradeCode
# Generate deterministic GUID from UpgradeCode (must match remediation script)
$guidBytes = [System.Text.Encoding]::UTF8.GetBytes($UpgradeCode)
$hash = [System.Security.Cryptography.MD5]::Create().ComputeHash($guidBytes)
$guid = "{$([System.Guid]::new($hash))}"
$registryBase = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\$guid"
# Step 1: Detect existence of Active Setup registry key
if (-not (Test-Path $registryBase)) {
Write-Host "Active Setup registry key not found: $registryBase"
return $null
}
# Step 2: Extract staged installer path from StubPath registry value
$stubPath = (Get-ItemProperty -Path $registryBase -Name "StubPath" -ErrorAction SilentlyContinue).StubPath
if (-not $stubPath) {
Write-Host "StubPath value missing from registry key."
return $null
}
# StubPath is in format: msiexec.exe /i "C:\ProgramData\ImmyBot\ActiveSetup\{UpgradeCode}\installer.msi" /qn /norestart
# Extract the quoted path from the StubPath value
$stagedFile = [regex]::Match($stubPath, '(?<="/)[^"]+(?=")').Value
# Re-add the leading quote character that the path starts after
$stagedFile = ([regex]::Match($stubPath, '"([^"]+)"')).Groups[1].Value
if (-not $stagedFile) {
Write-Host "Could not parse installer path from StubPath: $stubPath"
return $null
}
# Step 3: Detect existence of staged installer file
if (-not (Test-Path $stagedFile)) {
Write-Host "Staged installer file not found: $stagedFile"
return $null
}
# Step 4a: Extract version from staged MSI file
$windowsInstaller = New-Object -ComObject WindowsInstaller.Installer
$msiDatabase = $windowsInstaller.OpenDatabase($stagedFile, 0)
$view = $msiDatabase.OpenView("SELECT Value FROM Property WHERE Property = 'ProductVersion'")
$view.Execute()
$record = $view.Fetch()
$msiVersion = $record.StringData(1)
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($view) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($msiDatabase) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($windowsInstaller) | Out-Null
# Convert MSI dot version to Active Setup comma format
$versionParts = $msiVersion.Split('.')
while ($versionParts.Count -lt 4) { $versionParts += "0" }
$msiVersionFormatted = $versionParts[0..3] -join ","
# Step 4b: Get Active Setup version from registry
$activeSetupVersion = (Get-ItemProperty -Path $registryBase -Name "Version" -ErrorAction SilentlyContinue).Version
if (-not $activeSetupVersion) {
Write-Host "Active Setup Version key missing from registry."
return $null
}
# Step 5: Compare versions and return if matching
if ($msiVersionFormatted -eq $activeSetupVersion) {
Write-Host "Version match confirmed: $msiVersionFormatted"
return $msiVersion
} else {
Write-Host "Version mismatch - MSI: $msiVersionFormatted | Active Setup: $activeSetupVersion"
return $null
}
}