Как извлечь данные (количество файлов) из таблицы MSI "Файл"
В нашем процессе сборки в настоящее время существует вероятность того, что файлы, не основанные на коде (например, файлы изображений), будут добавлены в наш веб-проект, но не включены в установщик MSI, созданный WiX.
Чтобы предотвратить это, я хочу выполнить следующее в цели AfterBuild для нашего проекта WiX:
- Получить счетчик всех построенных файлов (вывод из проекта веб-развертывания)
- Получить количество всех файлов, встроенных в MSI (из таблицы "Файл" в MSI)
- Сравнить счетчики и не строить, если они не совпадают
Если я запускаю Orca, я легко могу увидеть таблицу файлов и сосчитать, но я не знаю, как автоматизировать это из MSBuild. Есть ли какой-нибудь API или другой механизм для получения этой информации из MSI?
Я не против написать пользовательскую задачу MSBuild для извлечения таблицы MSI File.
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);
Этот код использует WindowsInstaller.Installer
COM-объект, который является точкой входа для API автоматизации установщика Windows. Ознакомьтесь с полной справочной документацией.
редактировать: очевидно, 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);
MSI-файлы - это маленькие детские базы с собственным движком SQL. Вам просто нужно выполнить запрос:
SELECT `File` FROM `File`
и посчитать количество строк, которые возвращаются. Самым простым способом интеграции в MSBuild Task, вероятно, было бы использование DTX WiX, который предоставляет управляемые оболочки для всех API-интерфейсов MSI.
Решение будет действительно простым, как только вы получите все инструменты на месте.
Поскольку есть несколько способов реализовать это, я отвечаю на свой вопрос с результатами, которые я сейчас использую, благодаря ответам 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);
}
}
}
WinRAR идентифицирует MSI как самораспаковывающийся архив CAB (после предоставления ему расширения.rar). Я полагаю, вы могли бы скопировать файл куда-нибудь, переименовать его, распаковать его с WinRAR, а затем подсчитать файлы. Файлы не будут иметь свои оригинальные имена, хотя.
Это кажется немного устаревшим, и я не знаю, может ли это быть чем-то полезным.