如何从MSI“文件”表中提取数据(文件计数)

时间:2009-03-20 21:43:10

标签: wix windows-installer wix3 orca

在我们的构建过程中,目前有可能将非基于代码的文件(例如图像文件)添加到我们的Web项目中,但不包含在由WiX构建的MSI安装程序中。

为了帮助防止这种情况,我想在我们的WiX项目的AfterBuild目标中执行以下操作:

  • 获取构建的所有文件的计数(从Web部署项目输出)
  • 获取内置到MSI中的所有文件的计数(来自MSI中的“文件”表)
  • 比较计数和失败构建(如果它们不匹配)

如果我启动Orca,我可以很容易地看到File表和计数,但我不知道如何从MSBuild自动执行此操作。是否有一些API或其他机制可以从MSI中获取此信息?

我不介意编写自定义MSBuild任务来提取MSI文件表计数。

4 个答案:

答案 0 :(得分:8)

创建一个新的visual studio项目,添加对c:\windows\system32\msi.dll的引用,并使用以下代码读取msi文件中的文件数:

Type installerType = Type.GetTypeFromProgID("WindowsInstaller.Installer");
var installer =
   (WindowsInstaller.Installer)Activator.CreateInstance(installerType);
var msi = installer.OpenDatabase(@"path\to\some\file.msi", 0);
var fileView = msi.OpenView("SELECT FileName FROM File");
fileView.Execute(null);
int fileCount = 0;
while (fileView.Fetch() != null)
{
   fileCount++;
}
Console.WriteLine(fileCount);

此代码使用WindowsInstaller.Installer COM对象,它是Windows安装程序自动化API的入口点。看看complete reference documentation

编辑:显然wix附带了托管程序集(在C:\program files\Windows Installer XML v3\sdk中),它包含了msi.dll。我想这就是Rob在他的回答中所说的“DTF”。使用Microsoft.Deployment.WindowsInstaller程序集和命名空间中的类型可以简化代码示例:

var database = new Database(@"\path\to\some\file.msi");
var list = database.ExecuteQuery("SELECT FileName FROM File");
Console.WriteLine(list.Count);

答案 1 :(得分:4)

MSI文件是带有自定义SQL引擎的小宝贝数据库。您只需运行查询:

SELECT `File` FROM `File` 

并计算返回的行数。集成到MSBuild任务的最简单方法可能是使用WiX的DTF,它为所有MSI API提供托管包装。

一旦您获得了所有工具,解决方案将非常简单。

答案 2 :(得分:4)

由于有多种方法可以实现这一点,我正在回答我自己的问题,我现在正在使用的结果归功于wcoenen和Rob的答案。

这是自定义MSBuild任务:

public class VerifyMsiFileCount : Task
{
    [Required]
    public string MsiFile { get; set; }

    [Required]
    public string Directory { get; set; }

    public override bool Execute()
    {
       Database database = new Database(MsiFile, DatabaseOpenMode.ReadOnly);
        IList msiFiles = database.ExecuteQuery("SELECT FileName FROM File", new Record(0));
        IList<string> files = new List<string>(
            System.IO.Directory.GetFiles(Directory, "*", SearchOption.AllDirectories));
        return compareContents(msiFiles, files);
    }

    bool compareContents(IList msiFiles, IList<string> files)
    {
        // Always false if count mismatch, but helpful to know which file(s) are missing
        bool result = msiFiles.Count == files.Count;

        StringBuilder sb = new StringBuilder(msiFiles.Count);
        foreach (string msiFile in msiFiles)
        {
            sb.AppendLine(msiFile.ToUpper());
        }
        string allMsiFiles = sb.ToString();

        // Could be optimized using regex - each non-matched line in allMsiFiles
        string filename;
        foreach (string file in files)
        {
            filename = file.ToUpper();
            // Strip directory as File table in MSI does funky things with directory prefixing
            if (filename.Contains(Path.DirectorySeparatorChar.ToString()))
            {
                filename = filename.Substring(file.LastIndexOf(Path.DirectorySeparatorChar) + 1);
            }
            if (!allMsiFiles.Contains(filename))
            {
                result = false;
                MSBuildHelper.Log(this, file + " appears to be missing from MSI File table",
                    MessageImportance.High);
            }
        }
        return result;
    }
}

需要注意的事项:

  • 为了简洁,我遗漏了文档。
  • MSBuildHelper.Log只是ITask.BuildEngine.LogMessageEvent的一个简单包装器,用于捕获运行单元测试的NullReferenceException。
  • 仍有改进空间,例如:使用ITaskItem代替字符串作为属性,使用正则表达式进行比较。
  • 比较逻辑可能看起来有点奇怪,但File表做了一些带有目录前缀的时髦东西,我还想避免边缘情况,可能会删除文件并添加新文件,因此文件计数是正确但msi内容错误:)

以下是相应的单元测试,假设你的测试项目中有 Test.msi ,它被复制到输出目录。

[TestFixture]
public class VerifyMsiFileCountFixture
{
    VerifyMsiFileCount verify;

    [SetUp]
    public void Setup()
    {
        verify = new VerifyMsiFileCount();
    }

    [Test]
    [ExpectedException(typeof(InstallerException))]
    public void Execute_ThrowsInstallerException_InvalidMsiFilePath()
    {
        verify.Directory = Environment.CurrentDirectory;
        verify.MsiFile = "Bogus";
        verify.Execute();
    }

    [Test]
    [ExpectedException(typeof(DirectoryNotFoundException))]
    public void Execute_ThrowsDirectoryNotFoundException_InvalidDirectoryPath()
    {
        verify.Directory = "Bogus";
        verify.MsiFile = "Test.msi";
        verify.Execute();
    }

    [Test]
    public void Execute_ReturnsTrue_ValidDirectoryAndFile()
    {
        string directory = Path.Combine(Environment.CurrentDirectory, "Temp");
        string file = Path.Combine(directory, "Test.txt");
        Directory.CreateDirectory(directory);
        File.WriteAllText(file, "Temp");
        try
        {
            verify.Directory = directory;
            verify.MsiFile = "Test.msi";
            Assert.IsTrue(verify.Execute());
        }
        finally
        {
            File.Delete(file);
            Directory.Delete(directory);
        }
    }

    [Test]
    public void Execute_ReturnsFalse_NoFileDefined()
    {
        string directory = Path.Combine(Environment.CurrentDirectory, "Temp");
        Directory.CreateDirectory(directory);
        try
        {
            verify.Directory = directory;
            verify.MsiFile = "Test.msi";
            Assert.IsFalse(verify.Execute());
        }
        finally
        {
            Directory.Delete(directory);
        }
    }

    [Test]
    public void Execute_ReturnsFalse_IncorrectFilename()
    {
        string directory = Path.Combine(Environment.CurrentDirectory, "Temp");
        string file = Path.Combine(directory, "Bogus.txt");
        Directory.CreateDirectory(directory);
        File.WriteAllText(file, "Temp");
        try
        {
            verify.Directory = directory;
            verify.MsiFile = "Test.msi";
            Assert.IsFalse(verify.Execute());
        }
        finally
        {
            File.Delete(file);
            Directory.Delete(directory);
        }
    }

    [Test]
    public void Execute_ReturnsFalse_ExtraFileDefined()
    {
        string directory = Path.Combine(Environment.CurrentDirectory, "Temp");
        string file1 = Path.Combine(directory, "Test.txt");
        string file2 = Path.Combine(directory, "Bogus.txt");
        Directory.CreateDirectory(directory);
        File.WriteAllText(file1, "Temp");
        File.WriteAllText(file2, "Temp");
        try
        {
            verify.Directory = directory;
            verify.MsiFile = "Test.msi";
            Assert.IsFalse(verify.Execute());
        }
        finally
        {
            File.Delete(file1);
            File.Delete(file2);
            Directory.Delete(directory);
        }
    }
}

答案 3 :(得分:0)

WinRAR将MSI标识为自解压CAB存档(在为其提供.rar扩展名之后)。我想你可以将文件复制到某处,重命名,用WinRAR解压缩,然后计算文件。但是,文件不会有原始名称。

This似乎有点过时了,我不知道它是否有任何帮助。