級 MSI 打包實戰(zhàn):從靜默安裝到回滾驗證)
簡介Advanced Installer 22.5 打包 Windows 安裝包資源面向需要制作專業(yè)安裝程序的軟件開發(fā)者與運(yùn)維人員幫助將應(yīng)用程序、依賴文件及安裝配置整合為可安裝、可卸載、易管理的 Windows 安裝包。壓縮包共約 2000 個文件整體約 229.56MB涵蓋 623 個 png、321 個 jpg、136 個 ico 等界面與圖標(biāo)素材419 個 aip 工程文件、47 個 ail 本地化腳本、75 個 xsd 與 61 個 xml 配置定義以及 8 個 msi、1 個 msm 等安裝包樣本另含 rtf、html、xaml、ps1、cmd 等文檔與腳本便于直接參考工程結(jié)構(gòu)與多語言配置。已有 1432 人學(xué)習(xí)下載。資源覆蓋 GUI、靜默與命令行安裝模式包含安裝條件檢查、快捷方式與注冊表項設(shè)置等實踐素材適合對照官方發(fā)布說明研究 22.5 版本的新特性與打包選項快速掌握從工程配置到編譯發(fā)布的完整流程。1. 從一份能過企業(yè) IT 審核的安裝包說起做過 To B 交付的同行大概都有過這種經(jīng)歷代碼寫完了功能測通了結(jié)果卡在最后一步——客戶 IT 部門要求提供一個帶數(shù)字簽名、能靜默安裝、支持升級回滾、還得在 Windows Server 2016 上跑得起來的 MSI 包。用 Inno Setup 或 NSIS 硬扛腳本寫到懷疑人生用 Visual Studio 自帶的 Installer Projects功能又太單薄連個自定義安裝路徑的界面都做得磕磕絆絆。Advanced Installer 22.5 就是沖著這個場景來的它把 Windows InstallerMSI那套復(fù)雜到反人類的底層 API 封裝成了可視化工程同時保留了直接編輯 MSI 表的能力。換句話說新手可以拖拽生成安裝包熟手可以鉆進(jìn) Table Editor 里改每一行數(shù)據(jù)。這份資源適合兩類人一是需要給 Windows 桌面應(yīng)用做正規(guī)安裝包的開發(fā)者二是被 WiX 的 XML 語法折磨過、想找個折中方案的運(yùn)維工程師。它解決的核心問題不是“怎么把文件拷到 Program Files”而是“怎么讓安裝包符合企業(yè)軟件分發(fā)規(guī)范”。2. 工程結(jié)構(gòu)與 MSI 數(shù)據(jù)庫先搞懂它在替你做什么2.1 從 .aip 工程文件到 MSI 的編譯鏈路Advanced Installer 的工程文件后綴是.aip本質(zhì)是一個 XML 描述文件記錄了你配置的所有安裝邏輯。但最終交付物是.msi而 MSI 不是簡單的壓縮包——它是一個 OLE 復(fù)合文檔內(nèi)部嵌了一個關(guān)系型數(shù)據(jù)庫表名固定為Feature、Component、File、Registry、CustomAction等。你每在圖形界面點(diǎn)一次“添加文件”它就在File表和Component表里各插一行記錄并自動生成 GUID。編譯過程就是把這些表序列化進(jìn) MSI 流再附加 CAB 壓縮包。理解這條鏈路的意義在于當(dāng)安裝行為不符合預(yù)期時你不需要瞎猜直接看表就行。比如某個文件死活裝不進(jìn)去打開 Table Editor 查Component表里該文件的Attributes字段如果值是 0 而不是 256說明它被標(biāo)記為“本地僅安裝”在按需安裝模式下就不會釋放。常見做法是編譯前用“Validate”功能跑一遍 ICE 驗證它會告訴你哪張表違反了 Windows Installer 規(guī)范。2.2 三種安裝類型的選擇邏輯Advanced Installer 22.5 新建工程時讓你選架構(gòu)類型這不是隨便點(diǎn)的工程類型適用場景產(chǎn)物是否需要管理員權(quán)限Professional單機(jī)桌面應(yīng)用無域環(huán)境MSI EXE 引導(dǎo)通常需要Enterprise需要 AD 組策略分發(fā)MSI含 MST 變換需要Architect多語言、多產(chǎn)品套件MSI 捆綁包按配置選 Professional 就夠覆蓋 80% 的交付場景。如果你的客戶明確說“我們要用 SCCM 推送”那就必須選 Enterprise因為 SCCM 對 MSI 的靜默參數(shù)和日志記錄有額外要求Professional 生成的包在msiexec /qn下可能因為缺少ALLUSERS屬性而裝到當(dāng)前用戶目錄而不是全局目錄。2.3 用命令行編譯替代 GUI 點(diǎn)擊圖形界面適合調(diào)試但持續(xù)集成必須走命令行。Advanced Installer 提供了AdvancedInstaller.com這個 CLI 工具路徑通常在安裝目錄下。下面是一個典型的構(gòu)建腳本:: build_installer.bat :: 設(shè)置 Advanced Installer 安裝路徑 set AI_PATHC:\Program Files (x86)\Caphyon\Advanced Installer 22.5\bin\x86\AdvancedInstaller.com :: 重新構(gòu)建工程/rebuild 會先清理再編譯 %AI_PATH% /rebuild D:\projects\MyApp\MyApp.aip :: 檢查返回碼非 0 表示編譯失敗 if %errorlevel% neq 0 ( echo Build failed with error code %errorlevel% exit /b %errorlevel% ) :: 對生成的 MSI 做數(shù)字簽名需提前配置好證書 :: 注意signtool 來自 Windows SDK不是 Advanced Installer 自帶 signtool sign /f D:\certs\mycert.pfx /p password /fd SHA256 D:\projects\MyApp\MyApp-SetupFiles\MyApp.msi echo Build and sign completed.這段腳本的邏輯是先調(diào)/rebuild參數(shù)讓 Advanced Installer 以無界面模式重新編譯工程然后判斷errorlevel。這里有個血淚經(jīng)驗——AdvancedInstaller.com在編譯失敗時不一定返回非零值某些版本只會在日志里寫錯誤但返回 0。穩(wěn)妥做法是加一步檢查 MSI 文件的時間戳是否更新或者解析它輸出的日志文件。簽名步驟必須放在編譯之后因為任何對 MSI 的修改都會破壞已有簽名。參數(shù)說明/rebuild等價于先 Clean 再 Build適合 CI 環(huán)境如果只想增量編譯用/build。signtool的/fd SHA256指定文件摘要算法Windows 10 以后建議用 SHA256SHA1 簽名的包在較新系統(tǒng)上會彈“未知發(fā)布者”。3. 自定義操作與安裝界面把“下一步”變成可控流程3.1 用 Custom Action 在安裝前后跑腳本很多應(yīng)用安裝完需要注冊服務(wù)、寫環(huán)境變量、或者初始化數(shù)據(jù)庫。這些動作不能靠用戶手動做得用 Custom Action 嵌進(jìn) MSI 執(zhí)行序列。Advanced Installer 支持三種 Custom ActionEXE 調(diào)用、DLL 調(diào)用、JScript/VBScript 內(nèi)嵌腳本。我一般優(yōu)先用內(nèi)嵌 JScript因為它不依賴外部文件編譯進(jìn) MSI 后不會因為殺毒軟件攔截外部 EXE 而失敗。下面這段 JScript 放在“Custom Actions”頁的“Install”階段作用是安裝完成后在桌面創(chuàng)建快捷方式并設(shè)置一個注冊表標(biāo)記// CustomAction_Install.js // 此腳本在 MSI 的 InstallFinalize 之后執(zhí)行 // 參數(shù)通過 Session.Property 傳入 var shell new ActiveXObject(WScript.Shell); var fso new ActiveXObject(Scripting.FileSystemObject); // 獲取安裝目錄APPDIR 是 Advanced Installer 的內(nèi)置屬性 var installDir Session.Property(APPDIR); // 去掉末尾反斜杠 if (installDir.charAt(installDir.length - 1) \\) { installDir installDir.substring(0, installDir.length - 1); } // 創(chuàng)建桌面快捷方式 var desktop shell.SpecialFolders(Desktop); var shortcut shell.CreateShortcut(desktop \\MyApp.lnk); shortcut.TargetPath installDir \\MyApp.exe; shortcut.WorkingDirectory installDir; shortcut.Description MyApp Desktop Client; shortcut.Save(); // 寫注冊表標(biāo)記供后續(xù)升級判斷 var regPath HKCU\\Software\\MyCompany\\MyApp\\; shell.RegWrite(regPath InstalledVersion, Session.Property(ProductVersion), REG_SZ); // 返回 0 表示成功非 0 會導(dǎo)致安裝回滾 return 0;邏輯說明Session.Property(APPDIR)拿到用戶在界面選擇的安裝路徑這是 Advanced Installer 預(yù)定義的屬性。shell.SpecialFolders(Desktop)獲取當(dāng)前用戶桌面路徑注意這里用的是 HKCU 而不是 HKLM因為寫 HKLM 需要管理員權(quán)限而 Custom Action 默認(rèn)以模擬用戶身份運(yùn)行。如果一定要寫 HKLM需要在 Custom Action 屬性里勾選“Run under LocalSystem”。參數(shù)說明return 0是硬性要求返回非零值會讓 MSI 認(rèn)為自定義操作失敗觸發(fā)回滾用戶會看到“安裝程序被中斷”的報錯。調(diào)試階段可以在腳本里加Session.Log(message)把信息寫進(jìn) MSI 日志然后用msiexec /i MyApp.msi /l*v install.log查看。3.2 對話框序列的定制與屬性傳遞Advanced Installer 默認(rèn)的安裝界面是“歡迎 → 許可 → 安裝路徑 → 安裝 → 完成”。但企業(yè)客戶經(jīng)常要求加一個“服務(wù)器地址”輸入框讓安裝時就能配置后端連接。這需要在 Dialog Editor 里新建一個對話框放一個 Edit 控件綁定到自定義屬性比如SERVER_URL然后在“Custom Actions”里讀取這個屬性。操作步驟在“User Interface” → “Dialogs”里右鍵新建一個 Dialog設(shè)置其“Predecessor”為InstallDirDlg“Condition”留空表示總是顯示。拖入一個 Edit 控件在屬性面板的“Property Name”填SERVER_URL。接著在“Custom Actions”里加一個“Set installer property”動作把SERVER_URL的值寫進(jìn)配置文件。常見做法是用“Text File Update”功能直接替換配置文件里的占位符比寫腳本更穩(wěn)。這里有個容易翻車的地方屬性名必須全大寫且不能和 MSI 保留屬性沖突比如INSTALLDIR、TARGETDIR是系統(tǒng)保留的。如果你自定義了SERVER_URL在腳本里用Session.Property(SERVER_URL)讀取時如果用戶沒填返回的是空字符串而不是 undefined所以判斷要寫成if (url )而不是if (!url)。3.3 用 MST 變換實現(xiàn)多環(huán)境配置同一個 MSI 要裝到測試環(huán)境和生產(chǎn)環(huán)境區(qū)別只是配置文件里的數(shù)據(jù)庫連接串不同。如果打兩個包維護(hù)成本翻倍。正確做法是打一個基礎(chǔ) MSI再生成兩個 MSTTransform文件。Advanced Installer 的“Builds”功能支持這種模式在“Builds”頁新建兩個 Build分別設(shè)置不同的“Configuration”和輸出 MST。編譯后你會得到MyApp.msi、Test.mst、Prod.mst。安裝時用msiexec /i MyApp.msi TRANSFORMSTest.mst /qn就能應(yīng)用測試環(huán)境配置。MST 的本質(zhì)是一個差異數(shù)據(jù)庫只記錄與基礎(chǔ) MSI 不同的表行所以體積很小。注意MST 必須和 MSI 放在同一目錄或者用絕對路徑指定否則 msiexec 會報“找不到變換”。4. 避坑與排查那些讓我重裝三次系統(tǒng)的教訓(xùn)4.1 現(xiàn)象安裝包在 Win7 上報“不是有效的 Win32 應(yīng)用程序”原因Advanced Installer 22.5 默認(rèn)生成的引導(dǎo)程序EXE 外殼是 64 位的而 Win7 32 位系統(tǒng)無法運(yùn)行。雖然 MSI 本身是平臺無關(guān)的但外層 EXE 掛了。解決在“Builds”頁把“Package Type”從“EXE with resources”改成“MSI only”或者強(qiáng)制引導(dǎo)程序為 32 位。如果客戶堅持要 EXE 外殼在“Bootstrapper”設(shè)置里勾選“Build x86 version”。4.2 現(xiàn)象靜默安裝后程序能跑但開始菜單沒有快捷方式原因msiexec /qn模式下所有標(biāo)記為“Advertised”的快捷方式不會被創(chuàng)建。Advanced Installer 默認(rèn)把快捷方式設(shè)為“Advertised”因為這樣支持“按需安裝”。解決在“Shortcuts”頁選中快捷方式把“Advertised”屬性改為“No”?;蛘咴诿钚屑覣DDLOCALAll強(qiáng)制安裝所有功能。但注意ADDLOCALAll會安裝所有語言資源包體積會變大。4.3 現(xiàn)象升級安裝時提示“已安裝該產(chǎn)品的另一個版本”無法覆蓋原因MSI 的ProductCode變了但UpgradeCode沒變或者兩個都沒變。Windows Installer 靠UpgradeCode識別產(chǎn)品家族靠ProductCode識別具體版本。如果UpgradeCode相同但ProductCode也相同它會認(rèn)為你在裝同一個包。解決在“Product Details”頁確認(rèn)UpgradeCode保持不變整個產(chǎn)品生命周期都不變每次發(fā)版只改ProductCode和ProductVersion。Advanced Installer 有“Generate new ProductCode”按鈕每次構(gòu)建前點(diǎn)一下。另外在“Upgrades”頁配置“Custom Upgrade”指定舊版本范圍勾選“Uninstall old version first”。4.4 現(xiàn)象自定義操作里的腳本在 Win10 上正常在 Server 2016 上超時失敗原因Server 2016 默認(rèn)的 WSHWindows Script Host版本較老且 IE 增強(qiáng)安全配置可能禁用了 ActiveXObject。解決把 Custom Action 的類型從“JScript”改成“EXE”或“DLL”用 C# 寫一個控制臺程序編譯成 .NET Framework 4.6.2 版本Server 2016 自帶 4.6.2。在 Advanced Installer 里把這個 EXE 作為“Temporary File”嵌入執(zhí)行完自動刪除。注意 EXE 的入口參數(shù)要接收Session.Property傳過來的值通常用環(huán)境變量或命令行參數(shù)傳遞。4.5 現(xiàn)象數(shù)字簽名后安裝包體積暴漲原因簽名工具在 MSI 里嵌入了完整證書鏈如果證書鏈很長比如包含根證書、中間證書體積會增加幾百 KB。更嚴(yán)重的是某些時間戳服務(wù)器響應(yīng)慢導(dǎo)致簽名過程卡住。解決用signtool的/tr參數(shù)指定 RFC3161 時間戳服務(wù)器比老式/t更可靠。如果體積敏感可以在簽名后對 MSI 做一次“壓縮優(yōu)化”——Advanced Installer 的“Media”頁有“Compression”選項選“High”用 LZMA 算法但注意壓縮后的 MSI 在部分老舊系統(tǒng)上安裝會變慢。5. 進(jìn)階用 PowerShell 做安裝后驗證與回滾兜底5.1 安裝后自動驗證的腳本模板MSI 裝完不代表萬事大吉文件可能被殺毒軟件刪了服務(wù)可能沒起來。我習(xí)慣在 Custom Action 的最后一步調(diào)一個 PowerShell 腳本做自檢把結(jié)果寫進(jìn)日志文件。下面這個腳本檢查三個關(guān)鍵點(diǎn)主程序是否存在、服務(wù)是否運(yùn)行、注冊表鍵是否寫入。# Verify-Install.ps1 # 由 Custom Action 在 InstallFinalize 后調(diào)用 param( [string]$InstallDir $env:APPDIR, [string]$LogPath $env:TEMP\MyApp_InstallVerify.log ) $result () $result Verification started at $(Get-Date) # 檢查主程序文件 $exePath Join-Path $InstallDir MyApp.exe if (Test-Path $exePath) { $result PASS: Main executable found at $exePath } else { $result FAIL: Main executable missing } # 檢查 Windows 服務(wù)狀態(tài) $svc Get-Service -Name MyAppService -ErrorAction SilentlyContinue if ($svc -and $svc.Status -eq Running) { $result PASS: Service MyAppService is running } elseif ($svc) { $result FAIL: Service exists but status is $($svc.Status) } else { $result FAIL: Service not installed } # 檢查注冊表 $regPath HKLM:\SOFTWARE\MyCompany\MyApp if (Test-Path $regPath) { $ver (Get-ItemProperty -Path $regPath -Name Version -ErrorAction SilentlyContinue).Version $result PASS: Registry key found, version$ver } else { $result FAIL: Registry key missing } # 輸出結(jié)果 $result | Out-File -FilePath $LogPath -Encoding UTF8 $result | ForEach-Object { Write-Host $_ } # 如果有 FAIL返回非零值觸發(fā)回滾 if ($result -match FAIL) { exit 1 } else { exit 0 }邏輯說明腳本接收兩個參數(shù)$InstallDir默認(rèn)從環(huán)境變量APPDIR取這是 Advanced Installer 在調(diào)用外部腳本時自動注入的。Get-Service的-ErrorAction SilentlyContinue避免服務(wù)不存在時拋異常中斷腳本。最后用$result -match FAIL做整體判斷只要有一條失敗就exit 1。參數(shù)說明$LogPath默認(rèn)寫到 TEMP 目錄方便安裝失敗后讓用戶直接發(fā)日志給你。如果要在 MSI 回滾時也保留日志需要把日志路徑設(shè)到C:\ProgramData下因為 TEMP 目錄在回滾時可能被清理。注意 PowerShell 腳本的執(zhí)行策略——在 Server 2016 上默認(rèn)是 Restricted需要在 Custom Action 里用powershell.exe -ExecutionPolicy Bypass -File Verify-Install.ps1來調(diào)用。5.2 回滾兜底當(dāng)驗證失敗時自動卸載如果驗證腳本返回 1MSI 會觸發(fā)回滾但回滾只撤銷 MSI 自己做的更改你的 Custom Action 寫的注冊表、創(chuàng)建的服務(wù)可能殘留。穩(wěn)妥做法是在“Rollback”階段再加一個 Custom Action專門清理這些殘留。Advanced Installer 的“Custom Actions”頁支持設(shè)置“Rollback”時機(jī)把清理腳本掛上去。我一般會寫一個Cleanup-OnRollback.ps1內(nèi)容就是反向操作停服務(wù)、刪服務(wù)、刪注冊表鍵、刪安裝目錄。注意這個腳本必須用-ErrorAction SilentlyContinue包住所有命令因為回滾時環(huán)境可能已經(jīng)半損壞報錯會掩蓋真正的問題。從那以后我每次打 MSI 包都強(qiáng)制走一遍“編譯 → 簽名 → 在干凈虛擬機(jī)上靜默安裝 → 跑驗證腳本 → 故意讓驗證失敗測回滾”這個完整流程。少一步客戶現(xiàn)場就可能多一個通宵。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取