/// /// 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.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Xml; using JetBrains.Build.AllAssemblies; using JetBrains.Build.GuidCache; using JetBrains.Build.Omea.Infra; using JetBrains.Build.Omea.Resolved.Infra; using JetBrains.Build.Omea.Util; using Microsoft.Build.Framework; using Microsoft.Tools.WindowsInstallerXml.Serialize; using AssemblyName=System.Reflection.AssemblyName; using Directory=Microsoft.Tools.WindowsInstallerXml.Serialize.Directory; using File=Microsoft.Tools.WindowsInstallerXml.Serialize.File; namespace JetBrains.Build.Omea.Resolved.Tasks { /// /// Creates a WiX source with the product biaries described in it. /// public class WixProductBinariesResolved : TaskResolved { #region Data /// /// Prefix for the generated WiX element ID. /// public static readonly string DirectoryId = "D.ProductBinaries"; /// /// Prefix for the generated WiX element ID. /// public static readonly string FileComponentIdPrefix = "C.ProductBinaries"; /// /// Prefix for the generated WiX element ID. /// public static readonly string FileIdPrefix = "F.ProductBinaries"; public static readonly string PluginsRegistryKey = "Software\\JetBrains\\Omea\\Plugins"; /// /// Prefix for the generated WiX element ID. /// public static readonly string RegistryComponentIdPrefix = "C.ProductBinariesRegistry"; /// /// Prefix for the generated WiX element ID. /// public static readonly string RegistryValueIdPrefix = "R.ProductBinaries"; #endregion #region Implementation /// /// Create the Registry key for the Plugins section. /// private static void CreatePluginsRegistryKey(Component wixComponentRegistry) { var wixRegKeyPlugins = new RegistryKey(); wixComponentRegistry.AddChild(wixRegKeyPlugins); wixRegKeyPlugins.Id = string.Format("{0}.PluginsKey", RegistryValueIdPrefix); wixRegKeyPlugins.Action = RegistryKey.ActionType.createAndRemoveOnUninstall; wixRegKeyPlugins.Root = RegistryRootType.HKMU; wixRegKeyPlugins.Key = PluginsRegistryKey; } /// /// Checks whether the DLL defines an Omea plugin, and adds registration for it, if that is the case. /// private static void RegisterPlugin(AssemblyXml assemblyxml, File wixFileAssembly, Component wixComponentRegistry) { Assembly assembly = Assembly.Load(assemblyxml.Include); foreach(Type type in assembly.GetTypes()) { if(type.ContainsGenericParameters) continue; // Generics cannot be plugins if(type.FindInterfaces(delegate(Type m, object filterCriteria) { return (m.Name == "IPlugin") && (m.Assembly.GetName().Name == "OpenAPI"); }, null).Length == 0) continue; // Not a single plugin in this DLL // Yes, it's a plugin — produce registration in the Registry var wixRegValue = new RegistryValue(); wixComponentRegistry.AddChild(wixRegValue); wixRegValue.Id = string.Format("{0}.Plugin.{1}", RegistryValueIdPrefix, assemblyxml.Include); wixRegValue.Action = RegistryValue.ActionType.write; wixRegValue.Root = RegistryRootType.HKMU; wixRegValue.Key = PluginsRegistryKey; wixRegValue.Name = Regex.Replace(assemblyxml.Include, "(.+?)(Plugin)?", "$1"); wixRegValue.Type = RegistryValue.TypeType.@string; wixRegValue.Value = string.Format("[#{0}]", wixFileAssembly.Id); } } /// /// Writes a target file to the map, ensures that it's not duplicate. /// /// The file name, relative to the install root. /// Some textual comment on where the file is coming from. /// Map. private static void RegisterTargetFile(string name, string origin, Dictionary mapTargetFiles) { name = name.ToLowerInvariant(); string sOtherOrigin; if(mapTargetFiles.TryGetValue(name, out sOtherOrigin)) throw new InvalidOperationException(string.Format("The target file “{0}”is installed twice, first as “{1}”, then as “{2}”.", name, sOtherOrigin, origin)); mapTargetFiles.Add(name, origin); } private void HarvestPublisherPolicyAssemblies(AssemblyXml assemblyxml, Directory directory, ComponentGroup componentgroup, ref int nGeneratedComponents, Dictionary mapTargetFiles, GuidCacheXml guidcachexml) { if(!Bag.Get(AttributeName.IncludePublisherPolicy)) return; int nWasGeneratedComponents = nGeneratedComponents; var diFolder = new DirectoryInfo(Bag.GetString(AttributeName.ProductBinariesDir)); string sSatelliteWildcard = string.Format("Policy.*.{0}.{1}", assemblyxml.Include, "dll"); // Even an EXE assembly has a DLL policy file foreach(FileInfo fiPolicyAssembly in diFolder.GetFiles(sSatelliteWildcard)) { // Find the companion policy config file var fiPolicyConfig = new FileInfo(Path.ChangeExtension(fiPolicyAssembly.FullName, ".Config")); if(!fiPolicyConfig.Exists) throw new InvalidOperationException(string.Format("Could not locate the publisher policy config file for the assembly “{0}”; expected: “{1}”.", fiPolicyAssembly.FullName, fiPolicyConfig.FullName)); // We have to create a new component for each of the DLLs we'd like to GAC as publisher policy assemblies nGeneratedComponents++; // Create the component for the assembly (one per assembly) var component = new Component(); directory.AddChild(component); component.Id = string.Format("{0}.{1}", FileComponentIdPrefix, fiPolicyAssembly.Name); component.Guid = guidcachexml[assemblyxml.Include + " PublisherPolicy"].ToString("B").ToUpper(); component.DiskId = Bag.Get(AttributeName.DiskId); component.Location = Component.LocationType.local; // Register component in the group var componentref = new ComponentRef(); componentgroup.AddChild(componentref); componentref.Id = component.Id; // Add the assembly file (and make it the key path) var fileAssembly = new File(); component.AddChild(fileAssembly); fileAssembly.Id = string.Format("{0}.{1}", FileIdPrefix, fiPolicyAssembly.Name); fileAssembly.Name = fiPolicyAssembly.Name; fileAssembly.KeyPath = YesNoType.yes; fileAssembly.Checksum = YesNoType.yes; fileAssembly.Vital = YesNoType.no; fileAssembly.Assembly = File.AssemblyType.net; fileAssembly.ReadOnly = YesNoType.yes; RegisterTargetFile(fileAssembly.Name, string.Format("Publisher policy assembly file for the {0} product assembly.", assemblyxml.Include), mapTargetFiles); // Add the policy config file var filePolicy = new File(); component.AddChild(filePolicy); filePolicy.Id = string.Format("{0}.{1}", FileIdPrefix, fiPolicyConfig.Name); filePolicy.Name = fiPolicyConfig.Name; filePolicy.KeyPath = YesNoType.no; filePolicy.Checksum = YesNoType.yes; filePolicy.Vital = YesNoType.no; filePolicy.ReadOnly = YesNoType.yes; RegisterTargetFile(fileAssembly.Name, string.Format("Publisher policy configuration file for the {0} product assembly.", assemblyxml.Include), mapTargetFiles); } if(nWasGeneratedComponents == nGeneratedComponents) // None were actually collected throw new InvalidOperationException(string.Format("Could not locate the Publisher Policy assemblies for the “{0}” assembly. The expected full path is “{1}\\{2}”.", assemblyxml.Include, diFolder.FullName, sSatelliteWildcard)); } /// /// Harvests the satellite files for an assembly. /// /// Local name (without path, but with extension) of the file to seek for. /// How to treat the missing file. /// Display name of the satellite to be included into the error message. /// The parent component to mount the new entry into. /// The assembly for which we're seeking for satellites. private void HarvestSatellite(AssemblyXml assemblyxml, string sFileLocalName, IParentElement wixparent, MissingSatelliteErrorLevel errorlevel, string sSatelliteDisplayName, Dictionary mapTargetFiles) { if(sFileLocalName == null) throw new ArgumentNullException("sFileLocalName"); if(sSatelliteDisplayName == null) throw new ArgumentNullException("sSatelliteDisplayName"); if(wixparent == null) throw new ArgumentNullException("wixparent"); // Full path var fi = new FileInfo(Path.Combine(Bag.GetString(AttributeName.ProductBinariesDir), sFileLocalName)); // Missing? if(!fi.Exists) { string sErrorMessage = string.Format("Could not locate the {2} for the “{0}” assembly. The expected full path is “{1}”.", assemblyxml.Include, fi.FullName, sSatelliteDisplayName); switch(errorlevel) { case MissingSatelliteErrorLevel.None: break; // Ignore case MissingSatelliteErrorLevel.Warning: Log.LogWarning(sErrorMessage); break; case MissingSatelliteErrorLevel.Error: throw new InvalidOperationException(sErrorMessage); default: throw new ArgumentOutOfRangeException("errorlevel", errorlevel, "Oops."); } return; } // Create an entry var wixFile = new File(); wixparent.AddChild(wixFile); wixFile.Id = string.Format("{0}{1}", FileIdPrefix, fi.Name); wixFile.Name = fi.Name; wixFile.KeyPath = YesNoType.no; wixFile.Checksum = YesNoType.no; wixFile.Vital = YesNoType.no; RegisterTargetFile(wixFile.Name, string.Format("Satellite for the {0} product assembly.", assemblyxml.Include), mapTargetFiles); } /// /// Processes those AllAssemblies.Xml entries that are our own product assemblies. /// private int ProcessAssemblies(Directory wixDirectory, ComponentGroup wixComponentGroup, Component wixComponentRegistry, AllAssembliesXml allassembliesxml, Dictionary mapTargetFiles, GuidCacheXml guidcachexml) { // Collect the assemblies int nGeneratedComponents = 0; foreach(ItemGroupXml group in allassembliesxml.ItemGroup) { if(group.AllAssemblies == null) continue; foreach(AssemblyXml assemblyxml in group.AllAssemblies) { nGeneratedComponents++; FileInfo fiAssembly = FindAssemblyFile(assemblyxml); string sExtension = fiAssembly.Extension.TrimStart('.'); // The extension without a dot // Create the component for the assembly (one per assembly) var wixComponent = new Component(); wixDirectory.AddChild(wixComponent); wixComponent.Id = string.Format("{0}.{1}.{2}", FileComponentIdPrefix, assemblyxml.Include, sExtension); wixComponent.Guid = assemblyxml.MsiGuid; wixComponent.DiskId = Bag.Get(AttributeName.DiskId); wixComponent.Location = Component.LocationType.local; // Register component in the group var componentref = new ComponentRef(); wixComponentGroup.AddChild(componentref); componentref.Id = wixComponent.Id; // Add the assembly file (and make it the key path) var wixFileAssembly = new File(); wixComponent.AddChild(wixFileAssembly); wixFileAssembly.Id = string.Format("{0}.{1}.{2}", FileIdPrefix, assemblyxml.Include, sExtension); wixFileAssembly.Name = string.Format("{0}.{1}", assemblyxml.Include, sExtension); wixFileAssembly.KeyPath = YesNoType.yes; wixFileAssembly.Checksum = YesNoType.yes; wixFileAssembly.Vital = YesNoType.yes; wixFileAssembly.ReadOnly = YesNoType.yes; RegisterTargetFile(wixFileAssembly.Name, string.Format("The {0} product assembly.", assemblyxml.Include), mapTargetFiles); // Check whether it's a managed or native assembly AssemblyName assemblyname = null; try { assemblyname = AssemblyName.GetAssemblyName(fiAssembly.FullName); } catch(BadImageFormatException) { } // Add COM Self-Registration data if(assemblyxml.ComRegister) { /* foreach(ISchemaElement harvested in HarvestComSelfRegistration(wixFileAssembly, fiAssembly)) wixComponent.AddChild(harvested); */ SelfRegHarvester.Harvest(fiAssembly, assemblyname != null, wixComponent, wixFileAssembly); } // Ensure the managed DLL has a strong name if((assemblyname != null) && (Bag.Get(AttributeName.RequireStrongName))) { byte[] token = assemblyname.GetPublicKeyToken(); if((token == null) || (token.Length == 0)) throw new InvalidOperationException(string.Format("The assembly “{0}” does not have a strong name.", assemblyxml.Include)); } // Add PDBs if(Bag.Get(AttributeName.IncludePdb)) HarvestSatellite(assemblyxml, assemblyxml.Include + ".pdb", wixComponent, MissingSatelliteErrorLevel.Error, "PDB file", mapTargetFiles); // Add XmlDocs if((assemblyname != null) && (Bag.Get(AttributeName.IncludeXmlDoc))) HarvestSatellite(assemblyxml, assemblyxml.Include + ".xml", wixComponent, MissingSatelliteErrorLevel.Error, "XmlDoc file", mapTargetFiles); // Add configs HarvestSatellite(assemblyxml, assemblyxml.Include + "." + sExtension + ".config", wixComponent, (assemblyxml.HasAppConfig ? MissingSatelliteErrorLevel.Error : MissingSatelliteErrorLevel.None), "application configuration file", mapTargetFiles); HarvestSatellite(assemblyxml, assemblyxml.Include + "." + sExtension + ".manifest", wixComponent, (assemblyxml.HasMainfest ? MissingSatelliteErrorLevel.Error : MissingSatelliteErrorLevel.None), "assembly manifest file", mapTargetFiles); HarvestSatellite(assemblyxml, assemblyxml.Include + ".XmlSerializers." + sExtension, wixComponent, (assemblyxml.HasXmlSerializers ? MissingSatelliteErrorLevel.Error : MissingSatelliteErrorLevel.None), "serialization assembly", mapTargetFiles); // Add publisher policy assemblies if(assemblyname != null) HarvestPublisherPolicyAssemblies(assemblyxml, wixDirectory, wixComponentGroup, ref nGeneratedComponents, mapTargetFiles, guidcachexml); // Register as an OmeaPlugin if(assemblyname != null) RegisterPlugin(assemblyxml, wixFileAssembly, wixComponentRegistry); } } return nGeneratedComponents; } #endregion #region Overrides /// /// Actions under the resolver. /// protected override void ExecuteTaskResolved() { GuidCacheXml guidcachexml = 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 wixDirectory = new Directory(); // A locally created nameless directory that does not add any nested folders but defines the sources location wixDirectoryRef.AddChild(wixDirectory); wixDirectory.Id = DirectoryId; wixDirectory.FileSource = Bag.GetString(AttributeName.ProductBinariesDir); 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); // A component for the generated Registry entries var wixComponentRegistry = new Component(); wixDirectory.AddChild(wixComponentRegistry); wixComponentRegistry.Id = RegistryComponentIdPrefix; wixComponentRegistry.Guid = guidcachexml[GuidIdXml.MsiComponent_ProductBinaries_Registry_Hkmu].ToString("B").ToUpper(); wixComponentRegistry.DiskId = Bag.Get(AttributeName.DiskId); wixComponentRegistry.Location = Component.LocationType.local; var wixComponentRegistryRef = new ComponentRef(); wixComponentGroup.AddChild(wixComponentRegistryRef); wixComponentRegistryRef.Id = wixComponentRegistry.Id; // Create the Registry key for the Plugins section CreatePluginsRegistryKey(wixComponentRegistry); // Load the AllAssemblies file AllAssembliesXml allassembliesxml = AllAssembliesXml.LoadFrom(Bag.Get(AttributeName.AllAssembliesXml).ItemSpec); // Tracks the files on the target machine, to prevent the same file from being installed both as an assembly and as a reference var mapTargetFiles = new Dictionary(); int nGeneratedComponents = ProcessAssemblies(wixDirectory, wixComponentGroup, wixComponentRegistry, allassembliesxml, mapTargetFiles, guidcachexml); // 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} product binary components.", nGeneratedComponents); } #endregion #region MissingSatelliteErrorLevel Type /// /// Defines how to treat a missing satellite of the given type. /// protected enum MissingSatelliteErrorLevel { None, Warning, Error } #endregion } }