适用于 PowerShell 5.1 及以上版本
在企业环境中,软件部署是最常见的运维任务之一。MSI(Microsoft Installer)是 Windows 平台上标准的安装包格式,几乎所有企业级软件都提供 MSI 分发方式。手动安装不仅耗时,而且在批量部署场景下几乎不可行。通过 PowerShell 自动化 MSI 安装,可以实现静默安装、参数化配置、日志记录和错误处理,大幅提升部署效率。
随着 DevOps 和基础设施即代码(IaC)理念的普及,将软件安装纳入版本控制的自动化流程已成为标准实践。本文将介绍如何使用 PowerShell 调用 msiexec.exe 完成 MSI 包的自动化安装、卸载和状态检测。
基础:使用 msiexec 进行静默安装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
$msiPath = 'C:\Packages\example-app-2.0.msi' $logPath = 'C:\Logs\example-app-install.log'
$arguments = @( '/i', $msiPath '/qn' '/l*v', $logPath 'REBOOT=ReallySuppress' 'ALLUSERS=1' )
$process = Start-Process -FilePath 'msiexec.exe' ` -ArgumentList $arguments ` -Wait ` -PassThru
switch ($process.ExitCode) { 0 { Write-Host '安装成功' -ForegroundColor Green } 3010 { Write-Host '安装成功,需要重启' -ForegroundColor Yellow } 1602 { Write-Host '用户取消了安装' -ForegroundColor Yellow } 1603 { Write-Host '安装过程中发生严重错误' -ForegroundColor Red } default { Write-Host "安装返回代码:$_" -ForegroundColor Red } }
|
执行结果示例:
封装为可复用的安装函数
将安装逻辑封装为函数,方便在批量部署脚本中重复调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| function Install-MSIPackage {
[CmdletBinding()] param( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string]$Path,
[string]$LogPath,
[hashtable]$Properties,
[switch]$NoRestart )
if (-not $LogPath) { $packageName = [System.IO.Path]::GetFileNameWithoutExtension($Path) $logDir = 'C:\Logs\MSI' if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } $LogPath = Join-Path $logDir "$packageName-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" }
$args = @('/i', $Path, '/qn', '/l*v', $LogPath)
if ($NoRestart) { $args += 'REBOOT=ReallySuppress' }
if ($Properties) { foreach ($key in $Properties.Keys) { $args += "$key=$($Properties[$key])" } }
Write-Verbose "正在安装:$Path" Write-Verbose "日志文件:$LogPath" Write-Verbose "参数:$($args -join ' ')"
$process = Start-Process -FilePath 'msiexec.exe' ` -ArgumentList $args ` -Wait ` -PassThru
$result = [PSCustomObject]@{ MSIPath = $Path LogPath = $LogPath ExitCode = $process.ExitCode Success = $process.ExitCode -in @(0, 3010) Timestamp = Get-Date }
return $result }
|
调用封装好的函数:
1 2 3 4 5 6 7 8 9
| $result = Install-MSIPackage -Path 'C:\Packages\vendor-tool-3.5.msi' ` -Properties @{ INSTALLDIR = 'C:\Program Files\VendorTool' ADD_LOCAL = 'FeatureMain,FeatureCLI' } ` -NoRestart ` -Verbose
$result | Format-List
|
执行结果示例:
1 2 3 4 5 6 7 8 9
| 详细: 正在安装:C:\Packages\vendor-tool-3.5.msi 详细: 日志文件:C:\Logs\MSI\vendor-tool-3.5-20250818-090000.log 详细: 参数:/i C:\Packages\vendor-tool-3.5.msi /qn /l*v C:\Logs\MSI\vendor-tool-3.5-20250818-090000.log REBOOT=ReallySuppress INSTALLDIR=C:\Program Files\VendorTool ADD_LOCAL=FeatureMain,FeatureCLI
MSIPath : C:\Packages\vendor-tool-3.5.msi LogPath : C:\Logs\MSI\vendor-tool-3.5-20250818-090000.log ExitCode : 0 Success : True Timestamp : 2025/8/18 09:00:15
|
检测已安装的 MSI 产品
在安装之前,通常需要先检查目标软件是否已经安装,避免重复部署。Windows 注册表中保存了所有已安装的 MSI 产品信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| function Get-InstalledMSIProduct {
[CmdletBinding()] param( [string]$Name = '*' )
$uninstallPaths = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' )
$products = foreach ($path in $uninstallPaths) { if (Test-Path $path) { Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -and $_.WindowsInstaller -eq 1 } } }
$products | Where-Object { $_.DisplayName -like $Name } | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, PSChildName | Sort-Object DisplayName }
Get-InstalledMSIProduct -Name '*7-Zip*' | Format-Table -AutoSize
|
执行结果示例:
1 2 3
| DisplayName DisplayVersion Publisher InstallDate PSChildName ----------- -------------- --------- ----------- ----------- 7-Zip 24.09 24.09 Igor Pavlov 20250115 {23170F69-40C1-2702-2409-000001000000}
|
判断是否需要安装
结合版本比较逻辑,判断目标软件是否需要安装或升级。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| function Test-MSIInstallRequired {
[CmdletBinding()] param( [Parameter(Mandatory)] [string]$ProductName,
[Version]$MinVersion )
$installed = Get-InstalledMSIProduct -Name $ProductName
if (-not $installed) { Write-Verbose "产品 '$ProductName' 未安装,需要安装" return $true }
if ($MinVersion) { $currentVersion = [Version]::new(0, 0, 0, 0) if ($installed.DisplayVersion) { $currentVersion = [Version]$installed.DisplayVersion }
if ($currentVersion -lt $MinVersion) { Write-Verbose "当前版本 $currentVersion 低于要求版本 $MinVersion,需要升级" return $true }
Write-Verbose "当前版本 $currentVersion 已满足要求版本 $MinVersion,跳过安装" return $false }
Write-Verbose "产品 '$ProductName' 已安装,跳过" return $false }
$needInstall = Test-MSIInstallRequired -ProductName '*7-Zip*' -MinVersion '24.9' -Verbose Write-Host "需要安装: $needInstall"
|
执行结果示例:
1 2
| 详细: 当前版本 24.09 已满足要求版本 24.9,跳过安装 需要安装: False
|
批量部署多个 MSI 包
将上述函数组合起来,实现批量自动部署。这在初始化新服务器或统一升级软件时非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| $deployList = @( @{ Name = '*7-Zip*' MinVersion = '24.9' MSIPath = '\\fileserver\packages\7z2409-x64.msi' } @{ Name = '*Notepad++*' MinVersion = '8.7' MSIPath = '\\fileserver\packages\npp.8.7.Installer.x64.msi' } @{ Name = '*Visual Studio Code*' MinVersion = '1.95' MSIPath = '\\fileserver\packages\VSCodeSetup-x64-1.95.0.msi' } )
$results = foreach ($item in $deployList) { Write-Host "`n检查:$($item.Name)" -ForegroundColor Cyan
$needInstall = Test-MSIInstallRequired ` -ProductName $item.Name ` -MinVersion $item.MinVersion
if ($needInstall) { Write-Host "开始安装:$($item.MSIPath)" -ForegroundColor Yellow $result = Install-MSIPackage -Path $item.MSIPath -NoRestart $result | Add-Member -NotePropertyName 'ProductName' -NotePropertyValue $item.Name -PassThru } else { Write-Host "已满足要求,跳过" -ForegroundColor Green [PSCustomObject]@{ ProductName = $item.Name MSIPath = $item.MSIPath Success = $true ExitCode = -1 Message = '已安装且版本满足要求' } } }
Write-Host "`n===== 部署汇总 =====" -ForegroundColor Cyan $results | Select-Object ProductName, Success, ExitCode | Format-Table -AutoSize
|
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 检查:*7-Zip* 已满足要求,跳过
检查:*Notepad++* 开始安装:\\fileserver\packages\npp.8.7.Installer.x64.msi
检查:*Visual Studio Code* 开始安装:\\fileserver\packages\VSCodeSetup-x64-1.95.0.msi
===== 部署汇总 ===== ProductName Success ExitCode ----------- ------- -------- *7-Zip* True -1 *Notepad++* True 0 *Visual Studio Code* True 0
|
MSI 卸载操作
有时需要先卸载旧版本再安装新版本,或者清理不再使用的软件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| function Uninstall-MSIPackage {
[CmdletBinding()] param( [Parameter(Mandatory)] [string]$ProductName,
[string]$LogPath )
$product = Get-InstalledMSIProduct -Name $ProductName | Select-Object -First 1
if (-not $product) { Write-Warning "未找到产品 '$ProductName'" return }
if (-not $LogPath) { $logDir = 'C:\Logs\MSI' if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } $LogPath = Join-Path $logDir "uninstall-$($product.PSChildName)-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" }
$args = @( '/x', $product.PSChildName '/qn' '/l*v', $LogPath 'REBOOT=ReallySuppress' )
Write-Verbose "正在卸载:$($product.DisplayName) $($product.DisplayVersion)"
$process = Start-Process -FilePath 'msiexec.exe' ` -ArgumentList $args ` -Wait ` -PassThru
[PSCustomObject]@{ Product = $product.DisplayName Version = $product.DisplayVersion ExitCode = $process.ExitCode Success = $process.ExitCode -in @(0, 3010) LogPath = $LogPath } }
$uninstallResult = Uninstall-MSIPackage -ProductName '*OldTool*' -Verbose $uninstallResult | Format-List
|
执行结果示例:
1 2 3 4 5 6 7
| 详细: 正在卸载:OldTool 1.2.3 1.2.3
Product : OldTool 1.2.3 Version : 1.2.3 ExitCode : 0 Success : True LogPath : C:\Logs\MSI\uninstall-{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}-20250818-091500.log
|
解析 MSI 安装日志
安装失败时需要分析日志定位问题。以下是自动解析 MSI 日志的关键函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| function Get-MSIInstallError {
[CmdletBinding()] param( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string]$LogPath )
$logContent = Get-Content $LogPath -Encoding UTF8
$errorLines = $logContent | Select-String -Pattern 'Return value 3|错误|Error|ERROR' | Select-Object -First 20
$properties = $logContent | Select-String -Pattern '^Property\(S\):' | Select-Object -First 10
$returnCode = $logContent | Select-String -Pattern 'Return value [0-9]' | Select-Object -Last 1
[PSCustomObject]@{ LogFile = $LogPath ReturnCode = if ($returnCode) { ($returnCode.Line -replace '.*Return value (\d+).*', '$1') } else { '未知' } Errors = $errorLines | ForEach-Object { $_.Line } Properties = $properties | ForEach-Object { $_.Line } } }
$errorInfo = Get-MSIInstallError -LogPath 'C:\Logs\MSI\vendor-tool-3.5-20250818-090000.log' $errorInfo | Format-List
|
执行结果示例:
1 2 3 4
| LogFile : C:\Logs\MSI\vendor-tool-3.5-20250818-090000.log ReturnCode : 0 Errors : {} Properties : {Property(S): ProductCode = {B1C2D3E4-F5A6-7890-BCDE-F12345678901}...}
|
注意事项
- 权限要求:MSI 安装通常需要管理员权限,脚本应以提升模式运行(
#Requires -RunAsAdministrator)。
- 路径空格:MSI 文件路径中包含空格时,
msiexec 可能解析失败,建议使用短路径格式(Get-Item 的 Target 属性)或确保路径用引号包裹。
- 退出码含义:
0 表示成功,3010 表示成功但需要重启,1603 表示严重错误。应在脚本中对常见退出码做明确处理。
- 日志分析:生产环境中务必开启
/l*v 详细日志,安装失败时日志是排查问题的唯一依据。
- 并发安装:Windows Installer 不支持同时安装多个 MSI 包(MSI 引擎有全局锁),批量部署时应串行执行,避免并发冲突。
- 版本比较陷阱:注册表中的
DisplayVersion 字段格式不统一(有些是 1.2.3,有些是 1.2.3.4567),使用 [Version] 类型转换时要注意格式兼容性,建议用 TryParse 做容错处理。