为企业部署创建 Mozilla Firefox MSI






4.68/5 (9投票s)
使用 WIX 和 Powershell 创建 Mozilla Firefox MSI
引言
企业管理员经常会遇到(或希望)部署 Mozilla Firefox 的需求。由于 Firefox 目前尚未编译成 msi,因此需要将安装程序打包成 msi 文件。在本文中,我将提供一种为企业创建可自定义部署的方法。
背景
要求:Powershell、WIX、任何 Mozilla Firefox Setup 可执行文件和可选的自定义文件 local-settings.js、override.ini、mozilla.cfg、install (或 config) ini,以及 7zip 的命令行可执行文件和 SFX 模块 7zSD.sfx。
为了使此脚本正常运行,必须以提升的权限运行。我运行我大部分的脚本都是以提升的权限运行的,所以我最初的提交省略了这一点。这是“以管理员身份运行”选项的注册表导出:
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\runas]
[HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\runas\command]
@="\"c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe\" -file \"%1\""
此外,代码还可以实现安全检查。
function Has-Role {
param([Security.Principal.WindowsBuiltInRole]$Role = [Security.Principal.WindowsBuiltInRole]::Administrator)
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal $identity
return $principal.IsInRole($Role)
}
# Test For Admin Privileges
if((Has-Role) -ne $true) {
Write-Error "Elevated permissions are required."
Write-OutPut "Use an elevated command to complete these tasks."
return
}
使用代码
powershell 脚本设计为从将用于创建 msi 的所有文件所在的同一目录执行。它可以不带任何参数运行,因为安装可执行文件是必需的参数,所以可以在提示时输入此信息。
Param(
[Parameter(Position=0,Mandatory=$true)]
[String] $FirefoxSetupExecutable,
$ProductID = "*",
$InstallerName = "Firefox Setup",
$IconFile = "Icon.ico",
$CustomActionFile = "CustomAction.cs",
$WixFile = "Product.wxs")
- 参数声明包含:
- $FirefoxSetupExecutable = 来自 www.mozilla.org 的 Firefox 安装程序文件名
- $ProductID = Wix 安装程序的 Product Id
- $InstallerName = 修改后新的 FireFox 安装程序的名称。
- $IconFile = 用于“添加/删除程序”的图标。
- $CustomActionFile = 自定义操作的 C# 文件。
- $WixFile = 生成 msi 的 Wix 文件。
当默认值合适时,可以忽略这些参数。
其他声明:
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
# Upgrade code to allow removal of previous versions
Set-Variable -Name "upgradeguid" -Value "5DD94F3B-0E8D-46FF-A7D6-41D9E5576860"
Set-Variable -Name "namespaceURI" -Value "http://schemas.microsoft.com/wix/2006/wi"
Set-Variable -Name "targetFramework" -Value "v2.0"
# Build environment
Set-Variable -Name "buildEnv" -Value "x86"
# Temp extraction directory
Set-Variable -Name "workingdirectory" -Value "Customize"
# %wix%
Set-Variable -Name "wixDir" -Value ([System.Environment]::GetEnvironmentVariable("Wix"))
- 注意事项
- 所有文件必须相对于脚本:$scriptPath
- 在版本之间保持相同的升级代码可以移除先前版本。
- 当前的 Wix 构建使用 .Net 2,自定义操作 dll 必须针对此版本。
- Firefox 的构建环境是 x86。
- 必须安装 Wix。此脚本使用 wix 环境变量来定位 Wix 目录。
自定义 Firefox 安装程序
有关自定义 Firefox 的详细信息很容易获得,此处不作详细介绍。
我使用了:
local-settings.js
pref("general.config.obscure_value", 0);
pref("general.config.filename", "mozilla.cfg");
mozilla.cfg
// set Firefox Default homepage
pref("browser.startup.homepage","http://(company website)");
// disable default browser check
pref("browser.shell.checkDefaultBrowser", false);
pref("browser.startup.homepage_override.mstone", "ignore");
// disable application updates
pref("app.update.enabled", false);
// disables the 'know your rights' button from displaying on first run
pref("browser.rights.3.shown", true);
override.ini
[XRE]
EnableProfileMigrator=false
install.ini
[Install]
QuickLaunchShortcut=false
DesktopShortcut=false
MaintenanceService=false
这些是预期的文件,但并非必需。可以通过修改代码添加其他文件。
提取安装程序以进行自定义
function Customize-Installer
{
# Create custom installer for Firefox
if (Test-Path -Path "$scriptPath\$workingdirectory") {
Remove-Item -Path "$scriptPath\$workingdirectory" -Recurse -Force
}
if(Test-Path "$scriptPath\7za.exe") {
# Extract Firefox Setup x.x.exe
Write-Host "Extracting $FirefoxSetupExecutable"
$arg = [String]::Format('x "{0}\{1}" -o"{0}\{2}" -y', $scriptPath, $FirefoxSetupExecutable, $workingdirectory)
Start-Process -FilePath "$scriptPath\7za.exe" -ArgumentList $arg -Wait -WindowStyle Hidden
}
else {
throw "7za.exe missing."
return $false
}
将文件复制到目标目录
# Copy Customization
Write-Host "Customizing install."
$files = "install.ini", "override.ini", "mozilla.cfg", "local-settings.js"
$dirs = "$scriptPath\$workingdirectory", "$scriptPath\$workingdirectory\core\browser", "$scriptPath\$workingdirectory\core", "$scriptPath\$workingdirectory\core\defaults\pref"
$i = 0
do {
if(Test-Path ([String]::Format("{0}\{1}", $scriptPath, $files[$i]))) {
Copy-Item -Path ([String]::Format("{0}\{1}", $scriptPath, $files[$i])) -Destination ([String]::Format("{0}\{1}", $dirs[$i], $files[$i]))
}
$i++
}
until ($i -eq 4)
这里需要注意的文件是 install.ini。要调用 setup.exe 并带上 /INI 开关,需要提供 ini 文件的完整路径。此文件复制到 setup.exe 的相同位置,因此可以使用“.”来限定文件位置的路径。
创建新的 Firefox 安装程序
# Build Archive (Mozilla Source 1.0.2\7zip.bat)
Write-Host "Compressing files."
$arg = [String]::Format('a -t7z "{0}\Customize.7z" "{0}\{1}\*" -mx -m0=BCJ2 -m1=LZMA:d24 -m2=LZMA:d19 -m3=LZMA:d19 -mb0:1 -mb0s1:2 -mb0s2:3', $scriptPath, $workingdirectory)
Start-Process -FilePath "$scriptPath\7za.exe" -ArgumentList $arg -Wait -WindowStyle Hidden
使用 Firefox 1.0.2 版本的压缩设置,“临时”创建 zip 文件,其中包含自定义文件。
# Create Installer config file
Out-File -InputObject ";!@Install@!UTF-8!" -FilePath "$scriptPath\app.tag" -Encoding ascii # First Out-File Overwrites existing file
Out-File -InputObject 'Title="Mozilla Firefox"' -FilePath "$scriptPath\app.tag" -Encoding ascii -Append
if(Test-Path "$scriptPath\$workingdirectory\install.ini") {
Out-File -InputObject 'RunProgram="setup.exe /INI=.\install.ini"' -FilePath "$scriptPath\app.tag" -Encoding ascii -Append
}
else {
Write-Host "Default Setup..."
Out-File -InputObject 'RunProgram="setup.exe"' -FilePath "$scriptPath\app.tag" -Encoding ascii -Append
}
Out-File -InputObject ";!@InstallEnd@!" -FilePath "$scriptPath\app.tag" -Encoding ascii -Append
接下来,创建一个文件,让 SFX 执行 setup.exe 文件。如果使用了 install.ini 文件,则向 setup.exe 提供 /INI 开关;否则,setup.exe 在不带 ini 文件的情况下运行。
if(Test-Path "$scriptPath\$InstallerName.exe") {
Remove-Item "$scriptPath\$InstallerName.exe" -Force
}
Write-Host "Making $scriptPath\$InstallerName.exe"
# Self-extract archive for installers must be created as joining 3 files: SFX_Module, Installer_Config, 7z_Archive
Get-Content "$scriptPath\7zSD.sfx", "$scriptPath\app.tag", "$scriptPath\Customize.7z" -Encoding Byte -ReadCount 0 | Set-Content "$scriptPath\$InstallerName.exe" -Encoding Byte
return $true
} # End Customize-Installer
最后,合并 SFX 模块、标签和 zip 文件。此时,就构建了一个功能齐全的 Mozilla Firefox 设置可执行文件。(对于那些想自定义 Firefox 的人来说,这就是全部了。)
Custom Actions
C# 自定义操作 DLL
暂时离开 Powershell,转而介绍自定义操作...
CustomAction.cs
using System;
using Microsoft.Deployment.WindowsInstaller;
必需的导入——如果从本页复制代码,没有这些构建将失败。
namespace FirefoxInstaller
{
public class CustomActions
{
[CustomAction]
public static ActionResult FixQuote(Session session)
{
string propertyValue = session["QtExecCmdLine"];
if (propertyValue.Contains("\"\""))
{
session.Log("Too many quotes found.");
session["QtExecCmdLine"] = propertyValue.Replace("\"\"", "\"");
}
if (!propertyValue.Contains("\""))
{
// Add "
session.Log("Quotes missing.");
propertyValue = propertyValue.Replace(" " + session["HelperArgs"], "");
session["QtExecCmdLine"] = String.Format("\"{0}\" {1}", propertyValue, session["HelperArgs"]);
}
return ActionResult.Success;
}
先介绍这段“奇怪”的代码,我遇到一个问题,程序路径随机获得了太多的引号。
PROPERTY CHANGE: Modifying QtExecCmdLine property. Its current value is 'helper.exe -ms'. Its new value: '""C:\Program Files (x86)\Mozilla Firefox\uninstall\helper.exe"" -ms'. ... CAQuietExec: Error 0x80070057: Command failed to execute.
经过几个小时的搜索,我得出结论,绕过这个问题比修复它更快。(欢迎对此发表评论……)
[CustomAction]
public static ActionResult CASetQtExecCmdLine(Session session)
{
session.Log("Begin SetQtExecCmdLine");
string ffUk = session["FIREFOXUNINSTALLKEY"];
if (ffUk == null || ffUk == String.Empty)
{
session.Log("FIREFOXUNINSTALLKEY missing.");
return ActionResult.Failure;
}
string helper = Microsoft.Win32.Registry.GetValue(ffUk, "UninstallString", "\"[ProgramFilesFolder]Mozilla Firefox\\uninstall\\helper.exe\"").ToString();
session["QtExecCmdLine"] = String.Format("\"{0}\" {1}", helper, session["HelperArgs"]);
session.Log("End SetQtExecCmdLine");
return ActionResult.Success;
}
由于此配置将 Firefox 安装在默认位置,因此可以预期 helper.exe 位于默认路径。但是,可以修改 install.ini 来更改此位置。因此,更好的方法是通过卸载键来定位它(下一节介绍)。这也可以通过 Wix 自定义操作来实现,但查找注册表项仍然是一个要求——最好在检查注册表项时设置它。
[CustomAction]
public static ActionResult GetFirefoxUninstallKey(Session session)
{
session.Log("Begin GetFirefoxUninstallKey");
// Must have x86 for Wow6432Node access
object ffVersion = Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\Mozilla Firefox", "CurrentVersion", null);
if (ffVersion == null)
{
session.Log("CurrentVersion value not found.");
return ActionResult.Failure;
}
object ffUk = Microsoft.Win32.Registry.GetValue(String.Format(@"HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\Mozilla Firefox\{0}\Uninstall", ffVersion.ToString()), "Description", null);
if (ffUk == null)
{
session.Log("Description value not found.");
return ActionResult.Failure;
}
session["FIREFOXUNINSTALLKEY"] = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" + ffUk.ToString();
session.Log("End GetFirefoxUninstallKey");
return ActionResult.Success;
}
获取卸载键需要获取当前版本和卸载键的名称。
[CustomAction]
public static ActionResult HideFirefoxUninstallKey(Session session)
{
session.Log("Begin HideFirefoxUninstallKey");
string ffUk = session["FIREFOXUNINSTALLKEY"];
if (ffUk == null || ffUk == String.Empty)
{
session.Log("FIREFOXUNINSTALLKEY missing.");
return ActionResult.Failure;
}
try
{
Microsoft.Win32.Registry.SetValue(ffUk, "SystemComponent", 1, Microsoft.Win32.RegistryValueKind.DWord);
}
catch (Exception ex)
{
session.Log("Error setting registry value: " + ex.Message);
return ActionResult.Failure;
}
session.Log("End HideFirefoxUninstallKey");
return ActionResult.Success;
}
} // End class
} // End namespace
还需要一个额外的步骤来将 msi 与 Firefox 安装关联起来。那就是通过隐藏 Firefox 来在“添加/删除程序”中创建关联。这样,卸载 Firefox 就通过 msi 进行。另一种选择是在 ARP 中有两个条目,并且有可能在不卸载 msi 的情况下卸载 Firefox。
编译 dll
回到 Powershell 代码...
function Build-CustomActions
{
Param(
[Parameter(Position=0)] $CAfile)
if(!(Test-Path $CAfile)) {
throw "Missing custom actions"
return $false
}
if(Test-Path "$scriptPath\CustomAction.dll") {
Remove-Item "$scriptPath\CustomAction.dll" -Force
}
if(Test-Path "$scriptPath\CA.dll") {
Remove-Item "$scriptPath\CA.dll" -Force
}
Write-Host "Building custom actions."
$csImport = @'
public static System.CodeDom.Compiler.CompilerResults CompileCA(string frameworkVersion, string csFile, string dllFile, string platform, string wixWindowsInstaller)
{
System.CodeDom.Compiler.CompilerResults results = null;
Dictionary<string, string> d = new Dictionary<string, string>();
d.Add("CompilerVersion", frameworkVersion);
using (Microsoft.CSharp.CSharpCodeProvider p = new Microsoft.CSharp.CSharpCodeProvider(d))
{
System.CodeDom.Compiler.CompilerParameters parameters =
new System.CodeDom.Compiler.CompilerParameters(
new string[] { "System.dll",
wixWindowsInstaller }, dllFile);
parameters.TreatWarningsAsErrors = false;
parameters.CompilerOptions = "/platform:" + platform;
results = p.CompileAssemblyFromFile(parameters, csFile);
}
return results;
}
'@
$csCompiler = Add-Type -MemberDefinition $csImport -Name Compiler -Namespace CsCompiler -UsingNamespace System.Collections.Generic -PassThru
$result = $csCompiler::CompileCA("$targetFramework", "$CAfile", "$scriptPath\CustomAction.dll", "$buildEnv", "$wixDir\SDK\Microsoft.Deployment.WindowsInstaller.dll")
if($result.Errors.Count -gt 0) {
$result.Errors | % {
Write-Host $_.ErrorText
}
return $false
}
在遇到 Powershell v4 创建 CSharpCodeProvider 的重载编译器选项的问题后,我回退到先前版本中有效的 Add-Type。因为 Wix msi 将针对 Framework 2,所以 CSharpCodeProvider 必须为 v2.0 构建自定义操作文件。一些 C# 代码允许这样做。我包含了 Write-Host $_.ErrorText 来处理构建自定义操作时出现的问题。
# Compile for use in Wix MSI (Use default config)
Write-Host "Converting custom actions file."
$arg = [String]::Format('"{0}\CA.dll" "{1}\SDK\{2}\sfxca.dll" "{0}\CustomAction.dll" "{1}\SDK\MakeSfxCA.exe.config" "{1}\SDK\Microsoft.Deployment.WindowsInstaller.dll"', $scriptPath, $wixDir, $buildEnv)
$proc = Start-Process -FilePath "$wixDir\SDK\MakeSfxCA.exe" -ArgumentList $arg -Wait -PassThru -RedirectStandardOutput "$scriptPath\makesfxca.log" -WindowStyle Hidden
Get-Content "$scriptPath\makesfxca.log"
Remove-Item "$scriptPath\makesfxca.log"
if($proc.ExitCode -ne 0) {
throw "Error occured generating CA.dll"
Write-Host $proc.ExitCode
return $false
}
return $true
} # End Build-CustomActions
现在需要为 Wix 打包 C# dll。
Wix 文件
在介绍 Wix 文件如何在构建过程中更新之前,我们必须先介绍 Wix 文件。这不会是 Wix 的深度讨论。在 Wix Toolset 等网站上可以找到更多信息。
Windows Installation XML
Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
基本的 Wix 声明——xmlns 在本文后面为 XPath 带来了额外的挑战。
<Product Id="*" Name="Mozilla Firefox 28.0" Language="1033" Version="28.0.0.0" Manufacturer="Mozilla" UpgradeCode="5DD94F3B-0E8D-46FF-A7D6-41D9E5576860">
<Package InstallerVersion="300" Compressed="yes" InstallScope="perMachine" InstallPrivileges="elevated" />
<Media Id="1" Cabinet="install.cab" EmbedCab="yes" DiskPrompt="CD-ROM #1" />
<Property Id="DiskPrompt" Value="Mozilla Firefox [1]" />
<Property Id="PREVIOUSVERSIONSINSTALLED" Secure="yes" />
<Upgrade Id="5DD94F3B-0E8D-46FF-A7D6-41D9E5576860">
<UpgradeVersion Minimum="1.0.0.0" Maximum="28.0.0.0" Property="PREVIOUSVERSIONSINSTALLED" IncludeMinimum="yes" IncludeMaximum="no" />
</Upgrade>
要升级先前版本,需要升级表中的一个条目。这通过 Upgrade 元素来实现。
<Directory Id="TARGETDIR" Name="SourceDir" />
<DirectoryRef Id="TARGETDIR">
<!-- Dummy Component -->
<Component Id="A769DA8582654F72AA112C816C8CC998" Guid="39FB70F3-F95F-490D-BE6B-A75A4C3C7F96">
<RemoveFolder Id="ProgramMenuDir" On="uninstall" />
</Component>
</DirectoryRef>
<Feature Id="ProductFeature" Title="MozillaInstaller" Level="1">
<ComponentRef Id="A769DA8582654F72AA112C816C8CC998" />
</Feature>
此 Wix 文件包含一个“虚拟”目录和一个虚假的移除操作。这允许 msi 安装,唯一的实际载荷是二进制 Firefox 安装程序。稍后,在 Wix 构建过程中,仍然会生成关于 msi cab 文件不包含任何文件的警告。这是可以预料的。
<Binary Id="CA.dll" SourceFile="CA.dll" />
<Binary Id="SetupFile" SourceFile="Firefox Setup.exe" />
<Icon Id="Icon1" SourceFile="Icon.ico" />
以下是 msi 中包含的文件列表。自定义操作 dll、设置文件和图标是唯一使用的文件。
<Property Id="ARPPRODUCTICON" Value="Icon1" />
<Property Id="ARPNOMODIFY" Value="1" />
<Property Id="ARPNOREPAIR" Value="1" />
<Property Id="FIREFOXUNINSTALLKEY" Value=" " />
<Property Id="HelperArgs" Value="-ms" />
<Property Id="QtExecCmdLine" Value="helper.exe -ms" />
msi 的基本属性。截至 Firefox 28.0,安装程序仅允许卸载。为了与之对应,设置了 ARPNOMODIFY 和 ARPNOREPAIR,因此只能进行卸载。
<CustomAction BinaryKey="SetupFile" ExeCommand="/s" Execute="immediate" Id="CAinstaller" Return="check" />
<CustomAction Id="QtExecUninstall" BinaryKey="WixCA" DllEntry="CAQuietExec" Execute="immediate" Return="ignore" />
<CustomAction Id="CAHideFirefoxUninstallKey" Return="ignore" Execute="immediate" BinaryKey="CA.dll" DllEntry="HideFirefoxUninstallKey" />
<CustomAction Id="CAGetFirefoxUninstallKey" Return="ignore" Execute="immediate" BinaryKey="CA.dll" DllEntry="GetFirefoxUninstallKey" />
<CustomAction Id="CAGetFirefoxUninstallKey2" Return="ignore" Execute="immediate" BinaryKey="CA.dll" DllEntry="GetFirefoxUninstallKey" />
<CustomAction Id="CASetQtExecCmdLine" Return="ignore" Execute="immediate" BinaryKey="CA.dll" DllEntry="CASetQtExecCmdLine" />
<CustomAction Id="FixQuote" Return="ignore" Execute="immediate" BinaryKey="CA.dll" DllEntry="FixQuote" />
在 msi 中,向 Firefox 安装程序传递了一个附加参数:/S。这是为了隐藏提取窗口。(setup.exe 上的 /INI 开关使安装不可见。)
<InstallExecuteSequence>
<Custom Action="CAGetFirefoxUninstallKey" After="InstallValidate">Installed AND (REMOVE = "ALL")</Custom>
<Custom Action="CASetQtExecCmdLine" After="CAGetFirefoxUninstallKey">Installed AND (REMOVE = "ALL")</Custom>
<Custom Action="FixQuote" After="CASetQtExecCmdLine">Installed AND (REMOVE = "ALL")</Custom>
<Custom Action="QtExecUninstall" After="CASetQtExecCmdLine">Installed AND (REMOVE = "ALL")</Custom>
<RemoveExistingProducts Before="InstallInitialize" />
<Custom Action="CAinstaller" After="InstallFiles">NOT Installed</Custom>
<!-- InstallExecuteAgain -->
<Custom Action="CAGetFirefoxUninstallKey2" After="CAinstaller" />
<Custom Action="CAHideFirefoxUninstallKey" Before="InstallFinalize" />
</InstallExecuteSequence>
</Product>
</Wix>
RemoveExistingProducts 在 InstallInitialize 之前设置。(必须安排在此和 InstallValidate 之间。)最后需要的操作是定位卸载键,以便从 ARP 中隐藏实际的 Firefox 安装。为了卸载,在执行序列的早期也需要卸载键。
保持 Wix 文件更新
回到 Powershell 脚本...
在更新 Wix 文件之前,脚本需要知道正在使用的 Firefox 版本。这是通过 Get-Version 函数实现的,该函数定位当前从安装程序中提取的 firefox.exe 文件。
function Get-Version
{
# Get Firefox version info
if(Test-Path "$scriptPath\Customize\core\firefox.exe") {
return (Get-Item "$scriptPath\Customize\core\firefox.exe").VersionInfo.ProductVersion
}
}
function Update-WixFile
{
$xml = New-Object System.Xml.XmlDocument
$xml.Load("$scriptPath\$WixFile")
Write-Host "Updating $scriptPath\$WixFile."
$node = Select-Xml -Xml $xml.DocumentElement -XPath "//ns:Product" -Namespace @{"ns" = $namespaceURI}
$fVersion = Get-Version
$versionString = "0", "0", "0", "0"
#alter version string
$pl = $fVersion.Split(".")
$i = 0
while ($i -lt $pl.Length) {
$versionString[$i] = $pl[$i]
$i++
}
$productVersion = [String]::Join(".", $versionString)
还记得命名空间变量吗?使用 XPath 导航 Wix 文件需要它。xmlns 属性需要前缀节点名称。
此函数首先获取 Firefox 可执行文件的版本,并将其构建为升级表中所需的 Wix 版本字符串。
$node.Node.Attributes["Id"].Value = $ProductID
$node.Node.Attributes["UpgradeCode"].Value = $upgradeguid
$node.Node.Attributes["Name"].Value = "Mozilla Firefox $fVersion"
# update version
$node.Node.Attributes["Version"].Value = $productVersion
这里更新产品详细信息。最重要的是“添加/删除程序”中使用的 Name 和 Version 属性。
# upgrade info
$node = Select-Xml -Xml $xml.DocumentElement -XPath "//ns:Product/ns:Upgrade" -Namespace @{"ns" = $namespaceURI}
if($node -ne $null) {
$node.Node.Attributes["Id"].Value = $upgradeguid
}
$node = Select-Xml -Xml $xml.DocumentElement -XPath "//ns:Product/ns:Upgrade/ns:UpgradeVersion" -Namespace @{"ns" = $namespaceURI}
if($node -ne $null) {
$node.Node.Attributes["Maximum"].Value = $productVersion
}
更新当前构建的升级信息。每次构建之间必须使用相同的 Upgrade Code。可以使用 [guid]::NewGuid() 或 GUID 生成器(如 MS SDK 中的)来获取此值。
$node = Select-Xml -Xml $xml.DocumentElement -XPath "//ns:Product/ns:Binary[@Id='SetupFile']" -Namespace @{"ns" = $namespaceURI}
# update setup file name
$node.Node.Attributes["SourceFile"].Value = "$InstallerName.exe"
更新二进制文件名。(仅当未使用默认的 $InstallerName 时才需要。)
if(!(Test-Path "$scriptPath\$IconFile"))
{
# No Icon - Remove Icon Node
$node = Select-Xml -Xml $xml.DocumentElement -XPath "//ns:Product/ns:Icon" -Namespace @{"ns" = $namespaceURI}
if($node -ne $null) {
$node.Node.ParentNode.RemoveChild($node.Node) | Out-Null
}
$node = Select-Xml -Xml $xml.DocumentElement -XPath "//ns:Product/ns:Property[@Id='ARPPRODUCTICON']" -Namespace @{"ns" = $namespaceURI}
if($node -ne $null) {
$node.Node.ParentNode.RemoveChild($node.Node) | Out-Null
}
}
else
{
$node = Select-Xml -Xml $xml.DocumentElement -XPath "//ns:Product/ns:Icon" -Namespace @{"ns" = $namespaceURI}
if($node -ne $null) {
$node.Node.Attributes["SourceFile"].Value = "$IconFile"
}
else {
$node = $xml.CreateElement("Icon", $namespaceURI)
$node.Attributes.Append($xml.CreateAttribute("Id")) | Out-Null
$node.Attributes.Append($xml.CreateAttribute("SourceFile")) | Out-Null
$node.SetAttribute("Id", "Icon1")
$node.SetAttribute("SourceFile", "$IconFile")
$p = Select-Xml -Xml $xml.DocumentElement -XPath "//ns:Product" -Namespace @{"ns" = $namespaceURI}
$p.Node.AppendChild($node) | Out-Null
$node = Select-Xml -Xml $xml.DocumentElement -XPath "//ns:Product/ns:Property[@Id='ARPPRODUCTICON']" -Namespace @{"ns" = $namespaceURI}
if($node -eq $null) {
$node = $xml.CreateElement("Property", $namespaceURI)
$node.Attributes.Append($xml.CreateAttribute("Id")) | Out-Null
$node.Attributes.Append($xml.CreateAttribute("Value")) | Out-Null
$node.SetAttribute("Id", "ARPPRODUCTICON")
$node.SetAttribute("Value", "Icon1")
$p.Node.AppendChild($node) | Out-Null
}
else {
$node.Node.Attributes["Value"].Value = "Icon1"
}
}
}
$xml.Save("$scriptPath\$WixFile")
} # End Update-WixFile
最后一次更新是图标文件。最初的意图是包含一个函数来从可执行文件中提取图标。然而,这并不顺利。Windows API,如 CreateIconFromResourceEx(以及包装器 ExtractAssociatedIcon),对从 firefox.exe 中提取的图标进行了大量处理。最终,我使用了第三方工具从资源中提取并使用 VS 2012 进行了清理。此代码部分比其他部分大,因为它会根据图标文件的存在与否来修改 Wix 文件。ARP 中的图标需要两个元素:ARPPRODUCTICON 属性和 Icon 元素。
将 Wix 文件构建成 msi
function Build-Msi
{
if(Test-Path "$scriptPath\$InstallerName.msi") {
Remove-Item "$scriptPath\$InstallerName.msi" -Force
}
Write-Host "Building msi."
$arg = [String]::Format('"{0}\{1}" -out "{0}\{2}"', $scriptPath, $WixFile, $WixFile.Replace("wxs", "wixobj"))
Start-Process -FilePath "$wixDir\bin\candle.exe" -ArgumentList $arg -RedirectStandardOutput "$scriptPath\candle.log" -WindowStyle Hidden -Wait
Get-Content "$scriptPath\candle.log"
Remove-Item "$scriptPath\candle.log"
Write-Host ""
$arg = [String]::Format('"{0}\{1}" -ext WixUtilExtension.dll -out "{0}\{2}.msi"', $scriptPath, $WixFile.Replace("wxs", "wixobj"), $InstallerName)
$process = Start-Process -FilePath "$wixDir\bin\light.exe" -ArgumentList $arg -RedirectStandardOutput "$scriptPath\light.log" -WindowStyle Hidden -Wait
Get-Content "$scriptPath\light.log"
Remove-Item "$scriptPath\light.log"
}
要使其工作,最后一步是构建 msi。
清理
function Cleanup-Directory
{
Write-Host "Removing files."
# Custom Action
if(Test-Path "$scriptPath\CustomAction.dll") {
Remove-Item "$scriptPath\CustomAction.dll" -Force
}
if(Test-Path "$scriptPath\CA.dll") {
Remove-Item "$scriptPath\CA.dll" -Force
}
# Modification files
if (Test-Path -Path "$scriptPath\$workingdirectory") {
Remove-Item -Path "$scriptPath\$workingdirectory" -Recurse -Force
}
if (Test-Path -Path "$scriptPath\app.tag") {
Remove-Item -Path "$scriptPath\app.tag" -Force
}
if(Test-Path "$scriptPath\Customize.7z") {
Remove-Item "$scriptPath\Customize.7z" -Force
}
# Wix Files
$wFile = $WixFile.Replace(".wxs", "")
if (Test-Path "$scriptPath\$wFile.wixobj") {
Remove-Item "$scriptPath\$wFile.wixobj" -Recurse -Force
}
if (Test-Path "$scriptPath\$InstallerName.wixpdb") {
Remove-Item "$scriptPath\$InstallerName.wixpdb" -Recurse -Force
}
if (Test-Path "$scriptPath\$InstallerName.exe") {
Remove-Item "$scriptPath\$InstallerName.exe" -Recurse -Force
}
}
此外,保持工作区域整洁很有帮助。
脚本过程
$r = Customize-Installer
if($r -eq $false) {
Write-Host "Failed to customize installer."
return
}
$r = Build-CustomActions "$scriptPath\$CustomActionFile"
if($r -eq $false) {
Write-Host "Failed to build custom actions."
return
}
Update-WixFile
Build-Msi
Cleanup-Directory
Out-File -InputObject ([String]::Format('msiexec /i "%~dp0{0}.msi" /q /lvx* "%~dp0install.log"', $InstallerName)) -FilePath "$scriptPath\install.cmd" -Encoding ascii
Out-File -InputObject ([String]::Format('msiexec /x "%~dp0{0}.msi" /q /lvx* "%~dp0uninstall.log"', $InstallerName)) -FilePath "$scriptPath\uninstall.cmd" -Encoding ascii
执行脚本只需要几行。
包含用于安装和卸载 msi 的命令文件,以供测试。
关注点
可以通过复制本文中的代码来获取自定义文件、C# 代码、Wix 和 powershell。
图标文件必须从可执行文件中获取,或者可以使用自定义图标。此图标仅适用于“添加/删除程序”。
7zip 文件、Wix 和 Powershell 可从各自的网站获取。
请注意,Update-WixFile 和 Build-Msi 函数未执行返回检查。这并不是因为这些方法没有失败的可能。可以添加额外的错误检查,但在脚本执行的这一点上不需要。Xml 错误将由 Powershell 显示,即使格式良好的 Xml 在 Wix 构建过程中也可能失败——这些信息也将显示。
在本文的代码中,我选择将进程流重定向到一个文件并将其加载到 Powershell 主机。目前,获取标准输出需要创建一个进程并设置 processinfo——使用 Start-Process 更简单。
该项目使用组策略软件安装功能,在 Firefox 版本 21.0 和 28.0 上进行了测试。
历史
2014 年 4 月 20 日:我第一篇文章的初稿。
2014 年 4 月 20 日:更新了 Product.wxs 部分,增加了注释,并在 XmlDocument 方法 CreateElement 中添加了 NameSpace 参数。
2014 年 4 月 21 日:为 C# 文件包含了命名空间。还上传了 product.wxs、customaction.cs 和 Powershell 脚本。虽然可以通过复制代码来构建,但下载这些文件可能更容易。