65.9K
CodeProject 正在变化。 阅读更多。
Home

为企业部署创建 Mozilla Firefox MSI

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (9投票s)

2014 年 4 月 20 日

CPOL

8分钟阅读

viewsIcon

24355

使用 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")
  1. 参数声明包含:
  2. $FirefoxSetupExecutable = 来自 www.mozilla.org 的 Firefox 安装程序文件名
  3. $ProductID = Wix 安装程序的 Product Id
  4. $InstallerName = 修改后新的 FireFox 安装程序的名称。
  5. $IconFile = 用于“添加/删除程序”的图标。
  6. $CustomActionFile = 自定义操作的 C# 文件。
  7. $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 脚本。虽然可以通过复制代码来构建,但下载这些文件可能更容易。

© . All rights reserved.