Как извлечь данные (количество файлов) из таблицы файлов MSI

В нашем процессе сборки в настоящее время существует возможность добавления файлов, не основанных на коде (например, файлов изображений), в наш веб-проект, но не включенных в установщик MSI, созданный WiX.

Чтобы предотвратить это, я хочу выполнить следующее в цели AfterBuild для нашего проекта WiX:

  • Получите количество всех созданных файлов (вывод из проекта веб-развертывания)
  • Получите количество всех файлов, встроенных в MSI (из таблицы «Файл» в MSI)
  • Сравните счетчики и завершите сборку неудачно, если они не совпадают

Если я запустил Orca, я легко смогу увидеть таблицу файлов и подсчитать, но я не знаю, как автоматизировать это из MSBuild. Есть ли какой-то API или другой механизм для получения этой информации из MSI?

Я не против написать специальную задачу MSBuild для извлечения счетчика таблицы файлов MSI.


person si618    schedule 20.03.2009    source источник


Ответы (4)


Создайте новый проект 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);

В этом коде используется COM-объект WindowsInstaller.Installer, который является точкой входа для API автоматизации установщика Windows. Ознакомьтесь с полной справочной документацией.

edit: очевидно, что wix поставляется с управляемыми сборками (в C:\program files\Windows Installer XML v3\sdk), которые обертывают msi.dll. Я думаю, это то, что Роб имеет в виду «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);
person Wim Coenen    schedule 21.03.2009

Файлы MSI - это маленькие детские базы данных с настраиваемым механизмом SQL. Вам просто нужно выполнить запрос:

SELECT `File` FROM `File` 

и подсчитайте количество возвращающихся строк. Самым простым способом интеграции в задачу MSBuild, вероятно, было бы использование DTF WiX, который предоставляет управляемые оболочки для всех API-интерфейсов MSI.

Решение будет действительно простым, когда вы соберете все инструменты на свои места.

person Rob Mensching    schedule 21.03.2009
comment
+1 Я фанат wix, но никогда не замечал хороших качеств в папке C: \ program files \ Windows Installer XML v3 \ sdk, я добавил образец в свой ответ - person Wim Coenen; 21.03.2009
comment
Спасибо, Роб! Я отметил ответ wcoenen как правильный просто потому, что он добавил образец кода. Было бы неплохо отметить оба как правильные, но вы получили мой +1. - person si618; 23.03.2009

Поскольку есть несколько способов реализовать это, я отвечаю на свой вопрос результатами, которые сейчас использую, благодаря ответам 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);
        }
    }
}
person si618    schedule 23.03.2009

WinRAR идентифицирует MSI как самораспаковывающийся CAB-архив (после присвоения ему расширения .rar). Я полагаю, вы могли бы скопировать файл куда-нибудь, переименовать его, распаковать с помощью WinRAR, а затем посчитать файлы. Однако файлы не будут иметь своих исходных имен.

Это кажется немного устаревшим, и я не знаю, могло ли оно быть полезным.

person cdonner    schedule 21.03.2009
comment
Я не буду голосовать за это, так как это может действительно сработать, если WinRAR может читать файл структурированного хранилища COM (а это и есть файл MSI), но это определенно не способ подсчитывать количество файлов, посмотрите ответ Роба Меншинга в моем мнение. Если все, что вам нужно, это извлечь файлы, вы можете выполнить установку администратора из командной строки: setup.exe / a для exe-файла или msiexec / a YourMsiName.msi для файла MSI. - person Stein Åsmul; 19.05.2011