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

发布于 07-15 23:35 字数 387 浏览 11 评论 0原文

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

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

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

如果我启动 Orca,我可以轻松查看文件表和计数,但我不知道如何从 MSBuild 自动化此操作。 是否有一些 API 或其他机制可以从 MSI 中获取此信息?

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

In our build process there is currently the potential for non-code based files (such as image files) to be added to our web project, but not included in the MSI installer built by WiX.

To help prevent this, I want to perform the following in the AfterBuild target for our WiX project:

  • Get a count of all files built (output from web deployment project)
  • Get a count of all files built into MSI (from "File" table in MSI)
  • Compare counts and fail build if they don't match

If I fire up Orca I can easily see the File table and count, but I don't know how to automate this from MSBuild. Is there some API or other mechanism to get this information out of an MSI?

I don't mind writing a custom MSBuild task to extract the MSI File table count.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

旧时模样2024-07-22 23:35:56

创建一个新的 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 的入口点。 请查看完整参考文档

编辑:显然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);

Create a new visual studio project, add a reference to c:\windows\system32\msi.dll and use the following code to read the number of files in a msi file:

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);

This code uses the WindowsInstaller.Installer COM object, which is the entry-point for the windows installer automation API. Take a look at the complete reference documentation.

edit: apparently wix comes with managed assemblies (in C:\program files\Windows Installer XML v3\sdk) which wrap msi.dll. I guess this is what Rob is referring to by "DTF" in his answer. Using the types in the Microsoft.Deployment.WindowsInstaller assembly and namespace would simplify the code sample to this:

var database = new Database(@"\path\to\some\file.msi");
var list = database.ExecuteQuery("SELECT FileName FROM File");
Console.WriteLine(list.Count);
蓝海2024-07-22 23:35:56

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

SELECT `File` FROM `File` 

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

一旦所有工具就位,解决方案就会非常简单。

MSI files are little baby databases with a custom SQL engine. You just need to run the query:

SELECT `File` FROM `File` 

and count the number of rows that come back. Easiest way to integrate into an MSBuild Task would probably be to use WiX's DTF which provides managed wrappers for all of the MSI APIs.

The solution will be really simple once you get all the tools in place.

埖埖迣鎅2024-07-22 23:35:56

由于有多种方法可以实现此目的,因此我正在用我现在使用的结果来回答我自己的问题,这要感谢 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 而不是字符串作为属性,使用正则表达式进行比较。
  • 比较逻辑可能看起来有点奇怪,但是文件表在目录前缀方面做了一些有趣的事情,而且我还想避免可能删除文件并添加新文件的边缘情况,因此文件计数是正确的,但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);
        }
    }
}

Since there are multiple ways you could implement this, i'm answering my own question with the results I'm now using thanks to the answers from wcoenen and Rob.

This is the custom MSBuild task:

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;
    }
}

Couple of things to note:

  • I've left out documentation for brevity.
  • MSBuildHelper.Log is just a simple wrapper for ITask.BuildEngine.LogMessageEvent to catch NullReferenceException running unit tests.
  • Still room for improvement, e.g. using ITaskItem instead of string for properties, regex for comparison.
  • The comparison logic may look a little weird, but the File table does some funky stuff with directory prefixing, and I also wanted to avoid the edge case where a file may be deleted and a new file added, so the file count is correct but the msi contents are wrong :)

Here are the corresponding unit tests, assumption is you have Test.msi in your test project which is copied to the output directory.

[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);
        }
    }
}
转身泪倾城2024-07-22 23:35:56

WinRAR 将 MSI 识别为自解压 CAB 存档(在赋予其 .rar 扩展名后)。 我想您可以将文件复制到某处,重命名,用 WinRAR 解压,然后对文件进行计数。 不过,这些文件不会有原来的名称。

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

WinRAR identifies the MSI as a self-extracting CAB archive (after giving it a .rar extension). I suppose you could copy the file somewhere, rename it, unpack it with WinRAR, then count the files. The files will not have their original names, though.

This seems a bit outdated and I don't know if it could be of any help.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文