【问题标题】:How to replace a file in a msi installer?如何替换 msi 安装程序中的文件?
【发布时间】:2008-09-24 11:19:40
【问题描述】:

我想替换 msi 中的单个文件。怎么做?

【问题讨论】:

  • 请多解释。你的意思是你只有msi,你想改变一个文件并让它正常工作吗?

标签: windows-installer


【解决方案1】:

使用msi2xml

  1. 此命令提取 MSI 文件:

    msi2xml -c OutputDir TestMSI.MSI

  2. 打开OutputDir并修改文件。

  3. 重建 MSI 运行:

    xml2msi.exe -m TestMSI.xml

您需要 -m 来忽略 MSI 文件被修改时失败的“MD5 校验和测试”。

【讨论】:

  • 我还用blogs.technet.com/b/sateesh-arveti/archive/2010/11/21/… 找到了我要替换的文件的实际名称
  • 对我来说,它在 xml2msi 部分不断崩溃。公平地说,我尝试修改的安装程序庞大而复杂。
  • 我们如何在 Linux 机器上做到这一点?
  • 好工具。不过,2.2.0 版(不是最新版)对我有用。后来报了一些问题,重新打包不通
  • 请注意,msi2xml 需要到 OutputDir 的相对路径,而不是绝对路径。因此,如果您运行“msi2xml -c C:\MyPath TestMSI.MSI”,它将返回一个错误,指出 C:\MyPath 不是有效路径。但是,如果您键入“msi2xml -c .TestMSI.MSI”,它将正常工作并将 MSI 提取到当前 cmd 路径。
【解决方案2】:

您需要使用MsiDB.exe(随Windows Installer SDK 提供)从您的msi 中提取CAB 文件流。从带有 -x 选项的命令行运行它并指定 cab 文件的名称 - 这在 msi 数据库的 Media 表中列出。

或者,如果您将 VSI 选项中的“Package Files as:”选项指定为“Compresses in Cabinet Files”,则可以跳过此部分,以便在构建时将 cab 文件排除在 msi 之外(它将在与 msi 相同的目录)。

提取后,您可以更改 cab 文件夹中的指定文件 - 它的名称已被修改,因此您需要找出文件表中文件的 msi 名称,然后将新文件重命名为该名称。

完成后,您可以使用 MsiDB 实用程序使用 -a 选项将其弹出

在使用 -a 添加之前,您需要使用 msidb -kremove the cab from the MSI

【讨论】:

  • msidb -d test.msi -x Data1.cab 不工作?执行此命令后,没有错误没有成功信息。我不知道如何继续。
  • 您应该使用_Streams 表中显示的出租车名称。
【解决方案3】:

试试 InstEd - http://www.instedit.com/ 的安装程序编辑器。它有 30 天的试用期,对我有用。您将文件解压缩到一个文件夹,编辑、重建 cab,然后保存 MSI。除了编辑文件之外的所有操作都在 GUI 中完成。

不是一个很棒的程序,但我花了 30 美元才能够在 MSI 中快速编辑文件。

我不以任何方式为 InstEd 或相关机构工作,除了支付和使用该应用程序。

【讨论】:

  • 我的要求是一样的。但我无法使这个 Instedit 工作。如果您能告诉我要遵循的步骤,那就太好了。我需要替换我使用 Visual Studio 2010 创建的 msi 包中的 pdf 文件。
  • @ShaktiPrakashSingh 这已经晚了两年,但我刚刚使用 InstEdit 成功地使用 InstEdit 替换了我的 MSI 中的项目。我写在:stackoverflow.com/questions/4398042/replace-a-file-from-msi/…
【解决方案4】:

替换 MSI 中文件的非常简单的示例代码。这不会将新文件/CAB 流式传输回 MSI,但需要 CAB 与 MSI 位于同一目录中才能成功安装。我敢肯定,只要多花点功夫,您就可以更改代码以将 CAB 流式传输回来。

Const MSI_SOURCE = "application.msi"
Const FILE_REPLACE = "config.xml"

Dim filesys, installer, database, view
Dim objFile, size, result, objCab

Set filesys=CreateObject("Scripting.FileSystemObject")
Set installer = CreateObject("WindowsInstaller.Installer")
Set database = installer.OpenDatabase (MSI_SOURCE, 1)

Set objFile = filesys.GetFile(FILE_REPLACE)
size = objFile.Size

Set objCab = CreateObject("MakeCab.MakeCab.1")
objCab.CreateCab "config.cab", False, False, False
objCab.AddFile FILE_REPLACE, filesys.GetFileName(FILE_REPLACE)
objCab.CloseCab

Set view = database.OpenView ("SELECT LastSequence FROM Media WHERE DiskId = 1")
view.Execute

Set result = view.Fetch
seq = result.StringData(1) + 1 ' Sequence for new configuration file

Set view = database.OpenView ("INSERT INTO Media (DiskId, LastSequence, Cabinet) VALUES ('2', '" & seq & "', 'config.cab')")
view.Execute

Set view = database.OpenView ("UPDATE File SET FileSize = " & size & ", Sequence = " & seq & " WHERE File = '" & LCase(FILE_REPLACE) & "'")
view.Execute

【讨论】:

  • 您能否详细说明将 cab 文件流式传输到 msi 中?另外,如果我理解正确,您不是在 替换 msi 中的文件,而是在添加它。正确的? IOW,如果 'config.xml' 已经存在,那么现在将有两个。
  • objCab 是这样创建的,设置 objCab = CreateObject( "MakeCab.MakeCab.1" )。对吗?
  • 正确,我正在添加一个新文件,然后更新文件表以引用新添加的文件而不是原始文件。关于流媒体,MSI 可以包含嵌入(我认为)二进制表中的额外文件,例如 CAB 文件。虽然如果你将 CAB 推回 MSI,你会破坏任何数字签名 - 因此我将 CAB 分开。
【解决方案5】:

此代码仅在 1 个文件上进行了测试,其中名称与被替换的文件完全相同..

但它应该使用 DTF(来自 WIX)在 C# 中实现 Christopher Painters 答案

/**
 * this is a bastard class, as it is not really a part of building an installer package, 
 * however, we need to be able to modify a prebuild package, and add user specific files, post build, to save memory on server, and have a fast execution time.
 * 
 * \author Henrik Dalsager
 */

//I'm using everything...
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.Deployment.Compression.Cab;
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Deployment.WindowsInstaller.Package;

namespace MSIFileManipulator
{
/**
 * \brief updates an existing MSI, I.E. add new files
 * 
 */
class updateMSI
{
    //everything revolves around this package..
    InstallPackage pkg = null;

    //the destruction should close connection with the database, just in case we forgot..
    ~updateMSI()
    {
        if (pkg != null)
        {
            try
            {
                pkg.Close();
            }
            catch (Exception ex)
            {
                //rollback?

                //do nothing.. we just don't want to break anything if database was already closed, but not dereffered.
            }
        }
    }

    /**
     * \brief compresses a list of files, in a workdir, to a cabinet file, in the same workdir.
     * \param workdir path to the workdir
     * \param filesToArchive a list of filenames, of the files to include in the cabinet file.
     * \return filename of the created cab file
     */
    public string createCabinetFileForMSI(string workdir, List<string> filesToArchive)
    {
        //create temporary cabinet file at this path:
        string GUID = Guid.NewGuid().ToString();
        string cabFile = GUID + ".cab";
        string cabFilePath = Path.Combine(workdir, cabFile);

        //create a instance of Microsoft.Deployment.Compression.Cab.CabInfo
        //which provides file-based operations on the cabinet file
        CabInfo cab = new CabInfo(cabFilePath);

        //create a list with files and add them to a cab file
        //now an argument, but previously this was used as test:
        //List<string> filesToArchive = new List<string>() { @"C:\file1", @"C:\file2" };
        cab.PackFiles(workdir, filesToArchive, filesToArchive);

        //we will ned the path for this file, when adding it to an msi..
        return cabFile;
    }

    /**
     * \brief embeds a cabinet file into an MSI into the "stream" table, and adds it as a new media in the media table
     *  This does not install the files on a clients computer, if he runs the installer,
     *  as none of the files in the cabinet, is defined in the MSI File Table (that informs msiexec where to place mentioned files.)
     *  It simply allows cabinet files to piggypack within a package, so that they may be extracted again at clients computer.
     *  
     * \param pathToCabFile full absolute path to the cabinet file
     * \return media number of the new cabinet file wihtin the MSI
     */
    public int insertCabFileAsNewMediaInMSI(string cabFilePath, int numberOfFilesInCabinet = -1)
    {
        if (pkg == null)
        {
            throw new Exception("Cannot insert cabinet file into non-existing MSI package. Please Supply a path to the MSI package");
        }

        int numberOfFilesToAdd = numberOfFilesInCabinet;
        if (numberOfFilesInCabinet < 0)
        {
            CabInfo cab = new CabInfo(cabFilePath);
            numberOfFilesToAdd = cab.GetFiles().Count;
        }

        //create a cab file record as a stream (embeddable into an MSI)
        Record cabRec = new Record(1);
        cabRec.SetStream(1, cabFilePath);

        /*The Media table describes the set of disks that make up the source media for the installation.
          we want to add one, after all the others
          DiskId - Determines the sort order for the table. This number must be equal to or greater than 1,
          for out new cab file, it must be > than the existing ones...
        */
        //the baby SQL service in the MSI does not support "ORDER BY `` DESC" but does support order by..
        IList<int> mediaIDs = pkg.ExecuteIntegerQuery("SELECT `DiskId` FROM `Media` ORDER BY `DiskId`");
        int lastIndex = mediaIDs.Count - 1;
        int DiskId = mediaIDs.ElementAt(lastIndex) + 1;

        //wix name conventions of embedded cab files is "#cab" + DiskId + ".cab"
        string mediaCabinet = "cab" + DiskId.ToString() + ".cab";

        //The _Streams table lists embedded OLE data streams.
        //This is a temporary table, created only when referenced by a SQL statement.
        string query = "INSERT INTO `_Streams` (`Name`, `Data`) VALUES ('" + mediaCabinet + "', ?)";
        pkg.Execute(query, cabRec);
        Console.WriteLine(query);

        /*LastSequence - File sequence number for the last file for this new media.
          The numbers in the LastSequence column specify which of the files in the File table
          are found on a particular source disk.

          Each source disk contains all files with sequence numbers (as shown in the Sequence column of the File table)
          less than or equal to the value in the LastSequence column, and greater than the LastSequence value of the previous disk
          (or greater than 0, for the first entry in the Media table).
          This number must be non-negative; the maximum limit is 32767 files.
          /MSDN
         */
        IList<int> sequences = pkg.ExecuteIntegerQuery("SELECT `LastSequence` FROM `Media` ORDER BY `LastSequence`");
        lastIndex = sequences.Count - 1;
        int LastSequence = sequences.ElementAt(lastIndex) + numberOfFilesToAdd;

        query = "INSERT INTO `Media` (`DiskId`, `LastSequence`, `Cabinet`) VALUES (" + DiskId.ToString() + "," + LastSequence.ToString() + ",'#" + mediaCabinet + "')";
        Console.WriteLine(query);
        pkg.Execute(query);

        return DiskId;

    }

    /**
     * \brief embeds a cabinet file into an MSI into the "stream" table, and adds it as a new media in the media table
     *  This does not install the files on a clients computer, if he runs the installer,
     *  as none of the files in the cabinet, is defined in the MSI File Table (that informs msiexec where to place mentioned files.)
     *  It simply allows cabinet files to piggypack within a package, so that they may be extracted again at clients computer.
     *  
     * \param pathToCabFile full absolute path to the cabinet file
     * \param pathToMSIFile full absolute path to the msi file
     * \return media number of the new cabinet file wihtin the MSI
     */
    public int insertCabFileAsNewMediaInMSI(string cabFilePath, string pathToMSIFile, int numberOfFilesInCabinet = -1)
    {
        //open the MSI package for editing
        pkg = new InstallPackage(pathToMSIFile, DatabaseOpenMode.Direct); //have also tried direct, while database was corrupted when writing.
        return insertCabFileAsNewMediaInMSI(cabFilePath, numberOfFilesInCabinet);
    }

    /**
     * \brief overloaded method, that embeds a cabinet file into an MSI into the "stream" table, and adds it as a new media in the media table
     *  This does not install the files on a clients computer, if he runs the installer,
     *  as none of the files in the cabinet, is defined in the MSI File Table (that informs msiexec where to place mentioned files.)
     *  It simply allows cabinet files to piggypack within a package, so that they may be extracted again at clients computer.
     *
     * \param workdir absolute path to the cabinet files location
     * \param cabFile is the filename of the cabinet file
     * \param pathToMSIFile full absolute path to the msi file
     * \return media number of the new cabinet file wihtin the MSI
     */
    public int insertCabFileAsNewMediaInMSI(string workdir, string cabFile, string pathToMSIFile, int numberOfFilesInCabinet = -1)
    {
        string absPathToCabFile = Path.Combine(workdir, cabFile);
        string absPathToMSIFile = Path.Combine(workdir, pathToMSIFile);
        return insertCabFileAsNewMediaInMSI(absPathToCabFile, absPathToMSIFile, numberOfFilesInCabinet);
    }

    /**
     * \brief reconfigures the MSI, so that a file pointer is "replaced" by a file pointer to another cabinets version of said file...
     * The original file will not be removed from the MSI, but simply orphaned (no component refers to it). It will not be installed, but will remain in the package.
     * 
     * \param OriginalFileName (this is the files target name at the clients computer after installation. It is our only way to locate the file in the file table. If two or more files have the same target name, we cannot reorient the pointer to that file!)
     * \param FileNameInCabinet (In case you did not have the excact same filename for the new file, as the original file, you can specify the name of the file, as it is known in the cabinet, here.)
     * \param DiskIdOfCabinetFile - Very important information. This is the Id of the new cabinet file, it is the only way to know where the new source data is within the MSI cabinet stream. This function extracts the data it needs from there, like sequence numbers
     */
    public void PointAPreviouslyConfiguredComponentsFileToBeFetchedFromAnotherCabinet(string OriginalFileName, string FileNameInCabinet, string newFileSizeInBytes, int DiskIdOfCabinetFile)
    {
        //retrieve the range of sequence numbers for this cabinet file. 
        string query = "SELECT `DiskId` FROM `Media` ORDER BY `LastSequence`";
        Console.WriteLine(query);
        IList<int> medias = pkg.ExecuteIntegerQuery("SELECT `DiskId` FROM `Media` ORDER BY `LastSequence`");

        query = "SELECT `LastSequence` FROM `Media` ORDER BY `LastSequence`";
        Console.WriteLine(query); 
        IList<int> mediaLastSequences = pkg.ExecuteIntegerQuery("SELECT `LastSequence` FROM `Media` ORDER BY `LastSequence`");

        if(medias.Count != mediaLastSequences.Count)
        {
            throw new Exception("there is something wrong with the Media Table, There is a different number of DiskId and LastSequence rows");
        }

        if(medias.Count <= 0)
        {
            throw new Exception("there is something wrong with the Media Table, There are no rows with medias available..");
        }

        int FirstSequence = -1;
        int LastSequence = -1;
        int lastIndex = medias.Count - 1;

        for (int index = lastIndex; index >= 0; index--)
        {
            int rowLastSequence = mediaLastSequences.ElementAt(index);
            int rowDiskId = medias.ElementAt(index);

            if (rowDiskId == DiskIdOfCabinetFile)
            {
                LastSequence = rowLastSequence;
                if (index < lastIndex)
                {
                    //the next cabinet files last sequence number + 1,  is this ones first..
                    FirstSequence = mediaLastSequences.ElementAt(index + 1) + 1;
                    break;
                }
                else
                {
                    //all files from the first, to this last sequence number, are found in this cabinet
                    FirstSequence = mediaLastSequences.ElementAt(lastIndex);
                    break;
                }
            }
        }

        //now we will look in the file table to get a vacant sequence number in the new cabinet (if available - first run will return empty, and thus default to FirstSequence)
        int Sequence = FirstSequence;
        query = "SELECT `Sequence` FROM `File` WHERE `Sequence` >= " + FirstSequence.ToString() + " AND `Sequence` <= " + LastSequence.ToString() + " ORDER BY `Sequence`";
        Console.WriteLine(query);

        IList<int> SequencesInRange = pkg.ExecuteIntegerQuery(query);
        for (int index = 0; index < SequencesInRange.Count; index++)
        {
            if (FirstSequence + index != SequencesInRange.ElementAt(index))
            {
                Sequence = FirstSequence + index;
                break;
            }
        }

        //now we set this in the file table, to re-point this file to the new media..
        //File.FileName = FileNameInCabinet;
        //File.FileSize = newFileSizeInBytes;
        //File.Sequence = sequence;
        query = "UPDATE `File` SET `File`.`FileName`='" + FileNameInCabinet + "' WHERE `File`='" + OriginalFileName + "'";
        Console.WriteLine(query);
        pkg.Execute(query);
        query = "UPDATE `File` SET `File`.`FileSize`=" + newFileSizeInBytes + " WHERE `File`='" + OriginalFileName + "'";
        Console.WriteLine(query);
        pkg.Execute(query);
        query = "UPDATE `File` SET `File`.`Sequence`=" + Sequence.ToString() + " WHERE `File`='" + OriginalFileName + "'";
        Console.WriteLine(query);
        pkg.Execute(query);
    }        
}
}

示范用法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MSIFileManipulator
{
class Program
{
    static void Main(string[] args)
    {
        string workdir = @"C:\Users\Me\MyDevFolder\tests";
        string msiFile = "replace_test_copy.msi";
        string fileName = "REPLACE_THIS_IMAGE.png";

        List<string> filesToInclude = new List<string>();
        System.IO.FileInfo fileInfo = new System.IO.FileInfo(System.IO.Path.Combine(workdir, fileName));
        if (fileInfo.Exists)
        {
            Console.WriteLine("now adding: " + fileName + " to cabinet");
            filesToInclude.Add(fileName);

            updateMSI myMSI = new updateMSI();
            string cabfileName = myMSI.createCabinetFileForMSI(workdir, filesToInclude);
            Console.WriteLine("cabinet file saved as: " + cabfileName);

            int diskID = myMSI.insertCabFileAsNewMediaInMSI(workdir, cabfileName, msiFile);
            Console.WriteLine("new media added with disk ID: " + diskID.ToString());
            myMSI.PointAPreviouslyConfiguredComponentsFileToBeFetchedFromAnotherCabinet(fileName, fileName, fileInfo.Length.ToString(), diskID);
            Console.WriteLine("Done");

        }
        else
        {
            Console.WriteLine("Could not locate the replacement file:" + fileName);
        }
        Console.WriteLine("press any key to exit");
        Console.ReadKey();
    }
}
}

我知道我的测试在它自己之后没有清理..

【讨论】:

    【解决方案6】:

    最简单的方法是重新打包 MSI:

    1. 在 Wise for Windows Installer 中打开 MSI 文件。选择一个选项以提取文件。
    2. 找到磁盘上的文件并替换它。
    3. 构建 MSI。

    这些步骤也应该适用于 InstallShield。

    【讨论】:

    • 我猜对于有权使用商业重新打包工具的人来说是一个有效的解决方案......但如果是这样的话,我怀疑这个问题会被问到;)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-10
    • 2018-11-25
    • 1970-01-01
    相关资源
    最近更新 更多