【问题标题】:How to extract data (file count) from MSI "File" Table如何从 MSI“文件”表中提取数据(文件数)
【发布时间】:2010-10-14 15:57:10
【问题描述】:

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

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

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

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

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

【问题讨论】:

    标签: wix windows-installer wix3 orca


    【解决方案1】:

    由于您可以通过多种方式来实现这一点,我正在用我现在使用的结果来回答我自己的问题,这要感谢 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);
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

      新建一个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 带有包装 msi.dll 的托管程序集(在 C:\program files\Windows Installer XML v3\sdk 中)。我想这就是 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);
      

      【讨论】:

        【解决方案3】:

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

        SELECT `File` FROM `File` 
        

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

        一旦您准备好所有工具,解决方案将非常简单。

        【讨论】:

        • +1 我是 wix 粉丝,但从未注意到“C:\program files\Windows Installer XML v3\sdk”文件夹中的优点,我在自己的答案中添加了一个示例
        • 谢谢罗伯!我将 wcoenen 的答案标记为正确只是因为他添加了代码示例。最好将两者都标记为正确,但你得到了我的 +1。
        【解决方案4】:

        WinRAR 将 MSI 标识为自解压 CAB 存档(在为其提供 .rar 扩展名之后)。我想你可以将文件复制到某个地方,重命名它,用 WinRAR 解压,然后计算文件。不过,这些文件不会有原来的名称。

        This 好像有点过时了,不知道能不能帮上忙。

        【讨论】:

        • 我不会投反对票,因为如果 WinRAR 可以读取 COM 结构化存储文件(这是 MSI 文件是什么),它实际上可能会起作用,但这绝对不是获取文件计数的方法,在我看来,看看 Rob Mensching 的回答。如果您只想提取文件,您可以从命令提示符进行管理员安装:setup.exe /a 用于 exe 文件,或 msiexec /a YourMsiName.msi 用于 MSI 文件。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-09-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多