I want to replace a single file inside a msi. How to do it?
我想替换msi中的单个文件。怎么做?
6 个解决方案
#1
44
Use msi2xml.
-
This command extracts the MSI files:
此命令提取MSI文件:
msi2xml -c OutputDir TestMSI.MSI
msi2xml -c OutputDir TestMSI.MSI
-
Open OutputDir and modify the file.
打开OutputDir并修改文件。
-
To rebuild the MSI run:
要重建MSI运行:
xml2msi.exe -m TestMSI.xml
xml2msi.exe -m TestMSI.xml
You need the -m to ignore the 'MD5 checksum test' that fails when an MSIs file(s) are modified.
您需要-m忽略在修改MSI文件时失败的“MD5校验和测试”。
#2
17
You need to extract the CAB file stream from your msi using MsiDB.exe (supplied with the Windows Installer SDK). Run it from the command line with the -x option and specify the name of the cab file - this is listed in the Media table in the msi database.
您需要使用MsiDB.exe(随Windows Installer SDK提供)从msi中提取CAB文件流。使用-x选项从命令行运行它,并指定cab文件的名称 - 这将在msi数据库的Media表中列出。
Alternatively you can skip this part if you specify the "Package Files as:" option in the VSI options to "Compresses in Cabinet Files" to have the cab file left out of the msi when it's built (it will be created in the same directory as the msi).
或者,如果在VSI选项中将“Package Files as:”选项指定为“在Cabinet文件中压缩”,则可以跳过此部分,以便在构建时将cab文件从msi中删除(它将在同一目录中创建)作为msi)。
Once extracted you can change the specified file in the cab folder - its name has been mangled so you need to find out what msi name for the file is in the file table and then rename your new file to that.
解压缩后,您可以更改cab文件夹中的指定文件 - 其名称已被破坏,因此您需要找到该文件的msi名称在文件表中,然后将新文件重命名为该文件。
Once done you can pop it back in with the MsiDB utility using the -a option.
完成后,您可以使用-a选项使用MsiDB实用程序将其弹回。
Before you add with -a you need to use msidb -k to remove the cab from the MSI.
在使用-a添加之前,您需要使用msidb -k从MSI中删除cab。
#3
3
Try InstEd - an installer editor at http://www.instedit.com/. It has a 30 day trial, and it works for me. You extract the files to a folder, edit, rebuild the cab, and then save the MSI. Everything but the edit of your files is done in the GUI.
试试InstEd - http://www.instedit.com/上的安装程序编辑器。它有30天的试用期,它适用于我。您将文件解压缩到一个文件夹,编辑,重建cab,然后保存MSI。除了编辑文件之外的所有内容都在GUI中完成。
Not a great program but I paid my $30 to be able to quickly edit files in the MSI.
这不是一个很棒的程序,但我支付了30美元才能快速编辑MSI中的文件。
I don't work for InstEd or related in any way other than paying for and using the application.
除了支付和使用该应用程序之外,我不以任何方式为InstEd或相关工作。
#4
1
Very simple example code to replace a file inside an MSI. This does not stream the new file/CAB back into the MSI but requires the CAB to be in the same directory as the MSI for installation to succeed. I'm sure with a little more effort you could alter the code to stream the CAB back in.
用于替换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
#5
0
This code has only been tested on 1 file, where the name is excactly the same as the file being replaced..
此代码仅在1个文件上进行了测试,其中名称与要替换的文件完全相同。
but it should implement Christopher Painters answer in C#, with DTF (from WIX)
但它应该用C#实现Christopher Painters的答案,使用DTF(来自WIX)
/**
* 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);
}
}
}
demonstration usage:
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();
}
}
}
I am aware that my test does not clean up after it self..
我知道我的测试在自我测试后没有清理。
#6
-1
The easiest way to do it is to repackage MSI:
最简单的方法是重新打包MSI:
- Open MSI file in Wise for Windows Installer. Choose an option to to extract files.
- Locate the file on disk and replace it.
- Build MSI.
在Wise for Windows Installer中打开MSI文件。选择提取文件的选项。
在磁盘上找到该文件并替换它。
These steps should also work for InstallShield.
这些步骤也适用于InstallShield。
#1
44
Use msi2xml.
-
This command extracts the MSI files:
此命令提取MSI文件:
msi2xml -c OutputDir TestMSI.MSI
msi2xml -c OutputDir TestMSI.MSI
-
Open OutputDir and modify the file.
打开OutputDir并修改文件。
-
To rebuild the MSI run:
要重建MSI运行:
xml2msi.exe -m TestMSI.xml
xml2msi.exe -m TestMSI.xml
You need the -m to ignore the 'MD5 checksum test' that fails when an MSIs file(s) are modified.
您需要-m忽略在修改MSI文件时失败的“MD5校验和测试”。
#2
17
You need to extract the CAB file stream from your msi using MsiDB.exe (supplied with the Windows Installer SDK). Run it from the command line with the -x option and specify the name of the cab file - this is listed in the Media table in the msi database.
您需要使用MsiDB.exe(随Windows Installer SDK提供)从msi中提取CAB文件流。使用-x选项从命令行运行它,并指定cab文件的名称 - 这将在msi数据库的Media表中列出。
Alternatively you can skip this part if you specify the "Package Files as:" option in the VSI options to "Compresses in Cabinet Files" to have the cab file left out of the msi when it's built (it will be created in the same directory as the msi).
或者,如果在VSI选项中将“Package Files as:”选项指定为“在Cabinet文件中压缩”,则可以跳过此部分,以便在构建时将cab文件从msi中删除(它将在同一目录中创建)作为msi)。
Once extracted you can change the specified file in the cab folder - its name has been mangled so you need to find out what msi name for the file is in the file table and then rename your new file to that.
解压缩后,您可以更改cab文件夹中的指定文件 - 其名称已被破坏,因此您需要找到该文件的msi名称在文件表中,然后将新文件重命名为该文件。
Once done you can pop it back in with the MsiDB utility using the -a option.
完成后,您可以使用-a选项使用MsiDB实用程序将其弹回。
Before you add with -a you need to use msidb -k to remove the cab from the MSI.
在使用-a添加之前,您需要使用msidb -k从MSI中删除cab。
#3
3
Try InstEd - an installer editor at http://www.instedit.com/. It has a 30 day trial, and it works for me. You extract the files to a folder, edit, rebuild the cab, and then save the MSI. Everything but the edit of your files is done in the GUI.
试试InstEd - http://www.instedit.com/上的安装程序编辑器。它有30天的试用期,它适用于我。您将文件解压缩到一个文件夹,编辑,重建cab,然后保存MSI。除了编辑文件之外的所有内容都在GUI中完成。
Not a great program but I paid my $30 to be able to quickly edit files in the MSI.
这不是一个很棒的程序,但我支付了30美元才能快速编辑MSI中的文件。
I don't work for InstEd or related in any way other than paying for and using the application.
除了支付和使用该应用程序之外,我不以任何方式为InstEd或相关工作。
#4
1
Very simple example code to replace a file inside an MSI. This does not stream the new file/CAB back into the MSI but requires the CAB to be in the same directory as the MSI for installation to succeed. I'm sure with a little more effort you could alter the code to stream the CAB back in.
用于替换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
#5
0
This code has only been tested on 1 file, where the name is excactly the same as the file being replaced..
此代码仅在1个文件上进行了测试,其中名称与要替换的文件完全相同。
but it should implement Christopher Painters answer in C#, with DTF (from WIX)
但它应该用C#实现Christopher Painters的答案,使用DTF(来自WIX)
/**
* 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);
}
}
}
demonstration usage:
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();
}
}
}
I am aware that my test does not clean up after it self..
我知道我的测试在自我测试后没有清理。
#6
-1
The easiest way to do it is to repackage MSI:
最简单的方法是重新打包MSI:
- Open MSI file in Wise for Windows Installer. Choose an option to to extract files.
- Locate the file on disk and replace it.
- Build MSI.
在Wise for Windows Installer中打开MSI文件。选择提取文件的选项。
在磁盘上找到该文件并替换它。
These steps should also work for InstallShield.
这些步骤也适用于InstallShield。