using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Xml.Linq; namespace Centron.Scripts { public static class WXSHelper { private static readonly XNamespace Xmlns = @"http://schemas.microsoft.com/wix/2006/wi"; public static bool UpdateCentronSetupProductHeat() { return UpdateHeatWxs(CentronPaths.CentronNetInstaller.ProductHeat, CustomizeComponentInWxs, AllowIgnoreUpdatesToComponent, (string.Empty, CentronPaths.CentronNet.PublishDirectory), ("de", CentronPaths.CentronNet.PublishedDeDirectory), ("en", CentronPaths.CentronNet.PublishedEnDirectory), ("Tools", CentronPaths.CentronNet.PublishedToolsDirectory)); void CustomizeComponentInWxs(string fileName, XElement componentElement) { if (fileName == "Files\\CentronConnections.xml") { componentElement.SetAttributeValue("NeverOverwrite", "yes"); componentElement.SetAttributeValue("Permanent", "yes"); } } } public static bool UpdateWebServiceSetupProductHeat() { return UpdateHeatWxs(CentronPaths.WebServiceInstaller.ProductHeat, CustomizeComponentInWxs, AllowIgnoreUpdatesToComponent, (string.Empty, CentronPaths.WebService.PublishDirectory), ("de", CentronPaths.WebService.PublishedDeDirectory), ("en", CentronPaths.WebService.PublishedEnDirectory), ("development", CentronPaths.WebService.PublishedDevelopmentDirectory), ("ConnectionManager", CentronPaths.WebService.PublishedConnectionManagerDirectory), ("ConnectionManager\\de", CentronPaths.WebService.PublishedConnectionManagerDeDirectory), ("ConnectionManager\\en", CentronPaths.WebService.PublishedConnectionManagerEnDirectory)); void CustomizeComponentInWxs(string fileName, XElement componentElement) { if (fileName == "Files\\Centron.Host.WindowsService.exe") { var serviceInstallElement = new XElement(Xmlns + "ServiceInstall", new XAttribute("Id", "ServiceInstaller"), new XAttribute("Type", "ownProcess"), new XAttribute("Name", "CentronWebService"), new XAttribute("DisplayName", "c-entron Web-Service"), new XAttribute("Description", "c-entron Web-Service for access c-entron informations from outside"), new XAttribute("Start", "auto"), new XAttribute("ErrorControl", "normal")); componentElement.Add(serviceInstallElement); var serviceControlElement = new XElement(Xmlns + "ServiceControl", new XAttribute("Id", "StartService"), new XAttribute("Stop", "both"), new XAttribute("Remove", "uninstall"), new XAttribute("Name", "CentronWebService"), new XAttribute("Wait", "no"), new XAttribute("Start", "install")); componentElement.Add(serviceControlElement); // The Centron.Host.WindowsService.exe is referenced by this file-Id in the Product.wxs // So it always should stay this id: fil04C6E9A1324A0A1878612A211EC8F58D var fileElement = componentElement.Element(Xmlns + "File"); fileElement.SetAttributeValue("Id", "fil04C6E9A1324A0A1878612A211EC8F58D"); } } } private static bool AllowIgnoreUpdatesToComponent(XElement componentElement) { var fileNamesToIgnore = new[] { // This file contains a version-number, and is different depending on which .NET SDK is installed // So to allow building versions of the c-entron.NET and the Web-Services with .NET SDK version roll-forward, // we have to allow different versions of this file too // So we ignore WXS-heat updates to this file "Files\\mscordaccore_amd64_amd64_(.*).dll", "Files\\ConnectionManager\\mscordaccore_amd64_amd64_(.*).dll", // These nuget-packages contain the build-version number in their file-name // So we ignore WXS-heat updates to these files $"Files\\development\\{CentronPaths.CentronCore.ProjectName}.(.*).nupkg", $"Files\\development\\{CentronPaths.CentronInterfaces.ProjectName}.(.*).nupkg", $"Files\\development\\{CentronPaths.CentronWebServicesCore.ProjectName}.(.*).nupkg", // FastReport font cache files are excluded via ShouldExcludeFile, but we also need to // suppress removal warnings when these files are removed from existing heat files @"Files\\font(.*)\.list", }; var fileName = componentElement.Element(Xmlns + "File")?.Attribute("Source")?.Value ?? string.Empty; // Regex patterns see a single backslash as a escape-sequence, but in our case it's file-paths // So we replace single backslashes with double backslashes return fileNamesToIgnore.Any(f => Regex.IsMatch(fileName, f.Replace(@"\", @"\\"))); } private static bool UpdateHeatWxs(string filePath, Action componentModifier, Func allowIgnoreUpdatesToComponent, params (string directoryName, string directoryFilesPath)[] fileDirectories) { XDocument document; using (var fileStream = new FileStream(filePath, FileMode.Open)) { document = XDocument.Load(fileStream); } var fragments = document.Element(Xmlns + "Wix").Elements(Xmlns + "Fragment"); var directoryFragment = fragments.First(f => f.Element(Xmlns + "DirectoryRef") != null).Element(Xmlns + "DirectoryRef"); var componentGroupFragment = fragments.First(f => f.Element(Xmlns + "ComponentGroup") != null).Element(Xmlns + "ComponentGroup"); bool result = false; foreach (var (directoryName, directoryFilesPath) in fileDirectories) { result = result | UpdateComponentsForDirectory(directoryFragment, componentGroupFragment, directoryName, directoryFilesPath, componentModifier, allowIgnoreUpdatesToComponent); } OrderComponents(componentGroupFragment); using (var fileStream = new FileStream(filePath, FileMode.Truncate)) { document.Save(fileStream); } return result; } private static bool UpdateComponentsForDirectory(XElement directoryFragment, XElement rootComponentGroup, string directoryName, string directoryFilePath, Action componentModifier, Func allowIgnoreUpdatesToComponent) { string directoryGuid; if (string.IsNullOrWhiteSpace(directoryName)) directoryGuid = directoryFragment.Attribute("Id").Value; else { var parts = directoryName.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var current = (XElement)directoryFragment; foreach (var part in parts) { current = current.Elements(Xmlns + "Directory").First(f => f.Attribute("Name").Value == part); } directoryGuid = current.Attribute("Id").Value; } var files = Directory.Exists(directoryFilePath) ? Directory.GetFiles(directoryFilePath).Where(f => !ShouldExcludeFile(f)).ToList() : new List(); var removedFiles = RemoveComponentsForDirectory(directoryGuid, rootComponentGroup, files, allowIgnoreUpdatesToComponent); var addedFiles = AddComponentsForFiles(directoryGuid, directoryName, rootComponentGroup, files, componentModifier, allowIgnoreUpdatesToComponent); return removedFiles || addedFiles; } private static bool ShouldExcludeFile(string filePath) { var fileName = Path.GetFileName(filePath); // FastReport creates a font cache file (e.g. font2022.1.6.list) containing machine-specific font paths // This file is auto-generated at runtime and should not be included in the installer if (Regex.IsMatch(fileName, @"^font.*\.list$", RegexOptions.IgnoreCase)) return true; return false; } private static bool RemoveComponentsForDirectory(string directoryGuid, XElement rootComponentGroup, List files, Func allowIgnoreUpdatesToComponent) { var fileNames = files .Select(f => Path.GetFileName(f)) .ToList(); var components = rootComponentGroup .Elements(Xmlns + "Component") .Where(f => f.Attribute("Directory").Value == directoryGuid && fileNames.Contains(Path.GetFileName(f.Element(Xmlns + "File").Attribute("Source").Value)) == false) .ToList(); foreach (var currentComponent in components) { if (!allowIgnoreUpdatesToComponent(currentComponent)) { Console.WriteLine($"(ISSUE) Removed component: {currentComponent.Element(Xmlns + "File").Attribute("Source").Value}"); } currentComponent.Remove(); } return components.Any(f => allowIgnoreUpdatesToComponent(f) is false); } private static bool AddComponentsForFiles(string directoryGuid, string directoryName, XElement rootComponentGroup, IList files, Action componentModifier, Func allowIgnoreUpdatesToComponent) { var baseSource = Path.Combine("Files", directoryName); bool FileIsInWxs(string f) { return rootComponentGroup .Elements(Xmlns + "Component") .Any(d => d.Attribute("Directory").Value == directoryGuid && d.Element(Xmlns + "File").Attribute("Source").Value == Path.Combine(baseSource, Path.GetFileName(f))); } var missingFiles = files .Select(f => Path.GetFileName(f)) .Where(f => FileIsInWxs(f) == false) .ToList(); var addedComponents = new List(); foreach (var currentFile in missingFiles) { var fileSourcePath = Path.Combine(baseSource, currentFile); var component = new XElement(Xmlns + "Component", new XAttribute("Id", $"cmp{Guid.NewGuid().ToString("N").ToUpper()}"), new XAttribute("Directory", directoryGuid), new XAttribute("Guid", $"{Guid.NewGuid().ToString().ToUpper()}"), new XElement(Xmlns + "File", new XAttribute("Id", $"fil{Guid.NewGuid().ToString("N").ToUpper()}"), new XAttribute("KeyPath", "yes"), new XAttribute("Source", fileSourcePath))); componentModifier?.Invoke(fileSourcePath, component); rootComponentGroup.Add(component); addedComponents.Add(component); if (!allowIgnoreUpdatesToComponent(component)) { Console.WriteLine($"(ISSUE) Missing component in heat file: {component.Element(Xmlns + "File").Attribute("Source").Value}"); } } return addedComponents.Any(f => allowIgnoreUpdatesToComponent(f) is false); } private static void OrderComponents(XElement componentGroupFragment) { var components = componentGroupFragment .Elements(Xmlns + "Component") .OrderBy(f => f.Element(Xmlns + "File").Attribute("Source").Value) .ToList(); foreach (var component in components) { component.Remove(); componentGroupFragment.Add(component); } } } }