/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Xml; using JetBrains.Build.GuidCache; using JetBrains.Build.InstallationData; using JetBrains.Build.Omea.Infra; using JetBrains.Build.Omea.Resolved.Infra; using JetBrains.Omea.Base.Install; using Microsoft.Build.Framework; using Microsoft.Tools.WindowsInstallerXml.Serialize; using Directory=Microsoft.Tools.WindowsInstallerXml.Serialize.Directory; using File=Microsoft.Tools.WindowsInstallerXml.Serialize.File; #pragma warning disable SuggestBaseTypeForParameter namespace JetBrains.Build.Omea.Resolved.Tasks { /// /// Creates a WiX source with the product Registry data included in it. /// public class WixInstallationDataResolved : TaskResolved { #region Data /// /// Component ID for the component that is autogenerated. /// public static readonly string ComponentIdPrefix = "C.ProductInstallationData"; /// /// Directory ID for the directory that is autogenerated. /// public static readonly string DirectoryIdPrefix = "D.ProductInstallationData"; /// /// File ID for the file that is autogenerated. /// public static readonly string FileIdPrefix = "F.ProductInstallationData"; /// /// GUID database, loaded on execution. /// protected GuidCacheXml myGuidCache; /// /// Stores the components we've created for different Registry hives. /// protected readonly Dictionary myMapHiveToComponent = new Dictionary(); #endregion #region Implementation /// /// Converts the hive into a guid cache ID. /// protected static GuidIdXml GetGuidId(RegistryHiveXml hive) { switch(hive) { case RegistryHiveXml.Hkcr: return GuidIdXml.MsiComponent_RegistryData_Hkcr; case RegistryHiveXml.Hklm: return GuidIdXml.MsiComponent_RegistryData_Hklm; case RegistryHiveXml.Hkcu: return GuidIdXml.MsiComponent_RegistryData_Hkcu; case RegistryHiveXml.Hkmu: return GuidIdXml.MsiComponent_RegistryData_Hkmu; default: throw new ArgumentOutOfRangeException("hive", hive, "Unknown Registry Hive value."); } } /// /// Defines the macros that could be used in the Registry dumps. /// They must be substituted before writing into the WiX tables. /// WiX variables or MSI macros could still be used. /// protected static IDictionary GetMacros() { var macros = new Dictionary(); macros.Add(MacroNameXml.SystemDir.ToString(), "[SystemFolder]"); macros.Add(MacroNameXml.ProductBinariesDir.ToString(), "[INSTALLDIR]\\Bin"); macros.Add(MacroNameXml.DateTime.ToString(), "[Date]T[Time]"); return macros; } /// /// Converts a RegistryData hive into a WiX Root. /// protected static RegistryRootType GetRoot(RegistryHiveXml hivexml) { switch(hivexml) { case RegistryHiveXml.Hkcr: return RegistryRootType.HKCR; case RegistryHiveXml.Hklm: return RegistryRootType.HKLM; case RegistryHiveXml.Hkcu: return RegistryRootType.HKCU; case RegistryHiveXml.Hkmu: return RegistryRootType.HKMU; default: throw new InvalidOperationException(string.Format("Unexpected RegistryData hive “{0}”.", hivexml)); } } /// /// Converts a RegistryData value type into a WiX one. /// protected static RegistryValue.TypeType GetValueType(RegistryValueTypeXml type) { switch(type) { case RegistryValueTypeXml.Dword: return RegistryValue.TypeType.integer; case RegistryValueTypeXml.String: return RegistryValue.TypeType.@string; default: throw new InvalidOperationException(string.Format("Unknown RegistryData value type “{0}”.", type)); } } /// /// Creates a new WiX Component for populating with the values of a specific hive, or reuses an existing one. /// Registry items from different hives must not be mixed within a single component. /// /// Registry hive. /// A WiX directory to parent the newly-created component. /// A group of components to register the newly-created component into. protected Component GetComponentForHive(RegistryHiveXml hive, DirectoryRef directory, ComponentGroup componentgroup) { if(directory == null) throw new ArgumentNullException("directory"); if(componentgroup == null) throw new ArgumentNullException("componentgroup"); Component component; if(myMapHiveToComponent.TryGetValue(hive, out component)) // Lookup return component; // Reuse existing // Create a new one component = new Component(); myMapHiveToComponent.Add(hive, component); directory.AddChild(component); component.Id = string.Format("{0}.{1}", ComponentIdPrefix, hive); // Chose a GUID for the component, assign component.Guid = myGuidCache[GetGuidId(hive)].ToString("B").ToUpperInvariant(); // Register in the group var componentref = new ComponentRef(); componentgroup.AddChild(componentref); componentref.Id = component.Id; return component; } /// /// Processes the installation files, produces the WiX data. /// private int ConvertFiles(DirectoryRef wixDirectoryRef, ComponentGroup wixComponentGroup, InstallationDataXml dataxml, IDictionary macros) { int nProduced = 0; // Each installation folder derives a component, regardless of whether there are more files in the same folder, or not foreach(FolderXml folderxml in dataxml.Files) { folderxml.AssertValid(); // Create the component with the files var wixComponent = new Component(); wixComponent.Id = string.Format("{0}.{1}", ComponentIdPrefix, folderxml.Id); wixComponent.Guid = folderxml.MsiComponentGuid; wixComponent.DiskId = Bag.Get(AttributeName.DiskId); wixComponent.Location = Component.LocationType.local; ConvertFiles_AddToDirectory(folderxml, wixComponent, wixDirectoryRef); // To the directory structure // Add to the feature var wixComponentRef = new ComponentRef(); wixComponentRef.Id = wixComponent.Id; wixComponentGroup.AddChild(wixComponentRef); // To the feature var diSource = new DirectoryInfo(Path.Combine(LocalInstallDataResolved.ResolveSourceDirRoot(folderxml.SourceRoot, Bag), folderxml.SourceDir)); if(!diSource.Exists) throw new InvalidOperationException(string.Format("The source folder “{0}” does not exist.", diSource.FullName)); // Add files foreach(FileXml filexml in folderxml.Files) { filexml.AssertValid(); FileInfo[] files = diSource.GetFiles(filexml.SourceName); if(files.Length == 0) throw new InvalidOperationException(string.Format("There are no files matching the “{0}” mask in the source folder “{1}”.", filexml.SourceName, diSource.FullName)); if((files.Length > 1) && (filexml.TargetName.Length > 0)) throw new InvalidOperationException(string.Format("There are {2} files matching the “{0}” mask in the source folder “{1}”, in which case it's illegal to specify a target name for the file.", filexml.SourceName, diSource.FullName, files.Length)); foreach(FileInfo fiSource in files) { nProduced++; var wixFile = new File(); wixComponent.AddChild(wixFile); wixFile.Id = string.Format("{0}.{1}.{2}", FileIdPrefix, folderxml.Id, fiSource.Name).Replace('-', '_').Replace(' ', '_'); // Replace chars that are not allowed in the ID wixFile.Name = filexml.TargetName.Length > 0 ? filexml.TargetName : fiSource.Name; // Explicit target name, if present and if a single file; otherwise, use from source wixFile.Checksum = YesNoType.yes; wixFile.ReadOnly = YesNoType.yes; wixFile.Source = fiSource.FullName; } } } return nProduced; } /// /// Creates a new or uses the root directory to add the newly-created component with files being installed. /// private void ConvertFiles_AddToDirectory(FolderXml folderxml, Component wixComponent, DirectoryRef wixDirectoryRef) { if(folderxml.TargetRoot != TargetRootXml.InstallDir) throw new InvalidOperationException(string.Format("Only the InstallDir target root is supported.")); // No relative path — nothing to create if(folderxml.TargetDir.Length == 0) { wixDirectoryRef.AddChild(wixComponent); return; } // Create the folder structure, add to the innermost string[] arDirectoryChain = folderxml.TargetDir.Split('\\'); Directory wixParentDir = null; for(int a = 0; a < arDirectoryChain.Length; a++) { bool bInnermost = a == arDirectoryChain.Length - 1; // Whether this is the folder in which are the files itself // Create var wixDirectory = new Directory {Name = arDirectoryChain[a]}; // Mount self into the hierarchy if(wixParentDir != null) wixParentDir.AddChild(wixDirectory); // Previous dir else wixDirectoryRef.AddChild(wixDirectory); // The very root wixParentDir = wixDirectory; // Non-innermost folders get a suffix to their ID if(bInnermost) wixDirectory.Id = string.Format("{0}.{1}", DirectoryIdPrefix, folderxml.Id); else wixDirectory.Id = string.Format("{0}.{1}.P{2}", DirectoryIdPrefix, folderxml.Id, arDirectoryChain.Length - 1 - a); // Mount the component into the innermost dir if(bInnermost) wixDirectory.AddChild(wixComponent); } } /// /// Emits WiX Registry Keys from the installation data. /// private int ConvertRegistryKeys(DirectoryRef directory, ComponentGroup componentgroup, InstallationDataXml dataxml, IDictionary macros) { foreach(RegistryKeyXml keyxml in dataxml.Registry.Key) // Keys { try { var key = new RegistryKey(); GetComponentForHive(keyxml.Hive, directory, componentgroup).AddChild(key); key.Root = GetRoot(keyxml.Hive); key.Key = LocalInstaller.SubstituteMacros(macros, keyxml.Key); key.Action = RegistryKey.ActionType.createAndRemoveOnUninstall; } catch(Exception ex) { throw new InvalidOperationException(string.Format("Failed to process the key {0}. {1}", keyxml, ex.Message), ex); } } return dataxml.Registry.Key.Length; } /// /// Emits WiX Registry Values from the installation data. /// private int ConvertRegistryValues(DirectoryRef directory, ComponentGroup componentgroup, InstallationDataXml dataxml, IDictionary macros) { foreach(RegistryValueXml valuexml in dataxml.Registry.Value) { try { var value = new RegistryValue(); GetComponentForHive(valuexml.Hive, directory, componentgroup).AddChild(value); value.Root = GetRoot(valuexml.Hive); value.Key = LocalInstaller.SubstituteMacros(macros, valuexml.Key); if(!string.IsNullOrEmpty(valuexml.Name)) // The default value name must be Null not an empty string value.Name = LocalInstaller.SubstituteMacros(macros, valuexml.Name); value.Value = LocalInstaller.SubstituteMacros(macros, valuexml.Value); value.Type = GetValueType(valuexml.Type); value.Action = RegistryValue.ActionType.write; } catch(Exception ex) { throw new InvalidOperationException(string.Format("Failed to process the value {0}. {1}", valuexml, ex.Message), ex); } } return dataxml.Registry.Value.Length; } #endregion #region Overrides /// /// Actions under the resolver. /// protected override void ExecuteTaskResolved() { // Prepare the GUID cache myGuidCache = GuidCacheXml.Load(new FileInfo(Bag.GetString(AttributeName.GuidCacheFile)).OpenRead()); // Global structure of the WiX fragment file var wix = new Wix(); var wixFragmentComponents = new Fragment(); // Fragment with the payload wix.AddChild(wixFragmentComponents); var wixDirectoryRef = new DirectoryRef(); // Mount into the directories tree, defined externally wixFragmentComponents.AddChild(wixDirectoryRef); wixDirectoryRef.Id = Bag.GetString(AttributeName.WixDirectoryId); var wixFragmentGroup = new Fragment(); // Fragment with the component-group that collects the components wix.AddChild(wixFragmentGroup); var wixComponentGroup = new ComponentGroup(); // ComponentGroup that collects the components wixFragmentGroup.AddChild(wixComponentGroup); wixComponentGroup.Id = Bag.GetString(AttributeName.WixComponentGroupId); // Get the dump from the product InstallationDataXml dataxml = CreateInstaller().HarvestInstallationData(); IDictionary macros = GetMacros(); // Nullref guards dataxml.AssertValid(); // Convert into WiX int nProducedFiles = ConvertFiles(wixDirectoryRef, wixComponentGroup, dataxml, macros); int nProducedKeys = ConvertRegistryKeys(wixDirectoryRef, wixComponentGroup, dataxml, macros); int nProducedValues = ConvertRegistryValues(wixDirectoryRef, wixComponentGroup, dataxml, macros); // Save to the output file using(var xw = new XmlTextWriter(new FileStream(Bag.GetString(AttributeName.OutputFile), FileMode.Create, FileAccess.Write, FileShare.Read), Encoding.UTF8)) { xw.Formatting = Formatting.Indented; wix.OutputXml(xw); } // Report (also to see the target in the build logs) Log.LogMessage(MessageImportance.Normal, "Generated {0} files, {1} Registry keys, and {2} Registry values.", nProducedFiles, nProducedKeys, nProducedValues); } #endregion } }