//------------------------------------------------------------------------------ // // Copyright (c) John Robbins/Wintellect -- All rights reserved. // // // Wintellect Debugging .NET Code // //------------------------------------------------------------------------------ /*------------------------------------------------------------------------------ * See the following blog entries for more information about PARAFFIN: * * http://www.wintellect.com/cs/blogs/jrobbins/archive/2007/10/18/wix-hints-for-new-users-part-1-of-3.aspx * http://www.wintellect.com/cs/blogs/jrobbins/archive/2007/10/19/wix-the-pain-of-wix-part-2-of-3.aspx * http://www.wintellect.com/cs/blogs/jrobbins/archive/2007/10/21/wix-a-better-tallow-paraffin.aspx * * 1.00 - Initial release * 1.01 - Fixed a bug where directory and component names could have a dash in * them, which is not supported by WiX. * 1.02 - Special thanks to Darren Stone for all his input about PARAFFIN. * - Added -Win64 switch, which adds Win64="yes" to all components. * - Updated the Id naming to keep all values in the range [0-9a-zA-Z_] to * avoid any naming problems. WiX is not consistent on exactly what can * characters can be in the Id attribute. * - When updating, I was previously only relying on the Directory and * File elements Name attribute to find those elements. I mistakenly * thought the short file/directory name was guaranteed to be unique. * I fixed this bug by updating the Directory element searching to look * for either the matching Name attribute or LongName attribute depending * if the long name is different than the short name. For File elements, * I look at both the Name and the Source attributes for the exact match. * - Fixed a bug where I wasn't properly matching directory names when * generating the Id attribute. * - Fixed the innocuous bug where I was appending a double slash on an * alias if the input directory did not end in a trailing slash. * 1.03 - Fixed a bug where I was assuming that the short name for a file was * constant. It's really a random value. Now I only look at the Source * attribute when updating a File node as there's no other way to ensure * that a file is the same. This means I might have a rare conflict with * the short name for a file. The big reason for upgrading to WiX 3.0 is * that you no longer need to mess with these darn short names! * 1.04 - Thanks to Matthew Goos, added the -dirref option to allow a custom * name for the DirectoryRef node when creating a file. * - Now the -ext and direXclude command line options can also be specified * for updates in order to add additional extensions or directories to * ignore when updating a file. * 3.00 - Sorry for the big version jump but Paraffin now targets WiX 3.0 so I * thought I'd make them the same. Now that WiX 3.0 has hit beta, it's * time to support it. Note that this version no longer will create files * for use with WiX 2.0. However, it will import and convert previously * created Paraffin files to WiX 3.0. * Yay! No more short filenames! * - When adding files, I now check if they are .DLL, .EXE, or .OCX and if * so, add CheckSum='yes' attribute to the File element. * - All command line switches are now case insensitive. I'd forgotten to * set that in a prior version. -----------------------------------------------------------------------------*/ namespace Wintellect.Paraffin { using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Xml.Linq; using System.Runtime.InteropServices; using System.Globalization; using System.Diagnostics; using System.Collections; using System.Text.RegularExpressions; /// /// The main program. /// internal class Program { #region Comment Options Elements // All the elements for the data stored in the comment. private const String CMDLINEOPTIONSELEM = "CommandLineOptions"; private const String PRODUCEDBYELEM = "Producer"; private const String WARNINGELEM = "WARNING"; private const String DATECREATE = "CreatedOn"; private const String DIRECTORYELEM = "Directory"; private const String CUSTOMELEM = "Custom"; private const String ALIASELEM = "DirAlias"; private const String INCREMENTELEM = "Increment"; private const String GUIDSELEM = "Guids"; private const String MULTIPLEELEM = "Multiple"; private const String NORECURSELEM = "Norecurse"; private const String WIN64ELEM = "Win64"; private const String EXTEXCLUDEELEM = "ExtensionExcludes"; private const String EXTELEM = "Ext"; private const String DIREEXCLUDEELEM = "DirExcludes"; private const String DIREXT = "Dir"; private const String NEXTDIRECTORYNUMELEM = "NextDirectoryNumber"; private const String NEXTCOMPONENTNUMBER = "NextComponentNumber"; #endregion // The WiX 3.0 namespace. private static XNamespace nsWiX3 = "http://schemas.microsoft.com/wix/2006/wi"; private static XNamespace nsWiX2 = "http://schemas.microsoft.com/wix/2003/01/wi"; // The argument values used across all the methods. private static ParaffinArgParser argValues; // The current directory number. private static Int32 directoryNumber; // The starting directory name. I use this to build up unique // Directory Id values. private static String baseDirectoryName; // The full starting directory. If the user wants aliases, I'll replace // this with the alias. private static String fullStartDirectory; // The current component number private static Int32 componentNumber; // The error message. private static String errorMessage; // The input file namespace. private static XNamespace inputNameSpace = nsWiX3; // The PE file extensions. private static String [] peFileExtension = { ".DLL" , ".EXE" , ".OCX" }; internal static Int32 Main ( string [] args ) { // Think positive that everything will run completely. Int32 returnValue = 0; try { LoadProcessFeatureMap(); directoryNumber = 0; componentNumber = 0; errorMessage = String.Empty; argValues = new ParaffinArgParser(); if (args.Length > 0) { Boolean parsed = argValues.Parse(args); if (true == parsed) { if (true == argValues.Update) { returnValue = UpdateExistingFile(); } else { returnValue = CreateNewFile(); } } } else { argValues.OnUsage(String.Empty); returnValue = 1; } if (false == String.IsNullOrEmpty(errorMessage)) { Console.WriteLine(errorMessage); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); returnValue = -1; } return ( returnValue ); } /// /// Creates a brand new .WXS file for the specified directory and /// options. Any previous file of this name is overwritten. /// /// /// Zero if the file was all properly written. /// private static int CreateNewFile ( ) { // Create the XML document. XDocument doc = new XDocument ( ); // Add the WiX and Fragment nodes. XElement root = new XElement ( nsWiX3 + "Wix" ); doc.Add ( root ); XElement fragment = new XElement ( nsWiX3 + "Fragment" ); root.Add ( fragment ); // Add the DirectoryRef node. XElement directoryRef; if ( true == String.IsNullOrEmpty ( argValues.DirectoryRef ) ) { directoryRef = new XElement ( nsWiX3 + "DirectoryRef" , new XAttribute ( "Id" , "INSTALLDIR" ) ); } else { directoryRef = new XElement ( nsWiX3 + "DirectoryRef" , new XAttribute ( "Id" , argValues.DirectoryRef ) ); } fragment.Add ( directoryRef ); // Get the starting directories initialized. InitializeDirectoryValues ( ); // Now start the grind. RecurseDirectoriesForNewFile ( directoryRef , fullStartDirectory ); // Add the Component group node. AddComponentGroup ( fragment ); // Add the comment with all the command line options. AddCommandLineOptionsComment ( root ); ProcessFeatureMap(doc, Path.GetFileNameWithoutExtension(argValues.FileName) ); ProcessFeaturePatternMap(doc); // We're done, save it! doc.Save ( argValues.FileName ); return ( 0 ); } private static Dictionary _identifierOverrides = new Dictionary(); private static Dictionary _featureMap = new Dictionary(); private static Dictionary _featurePatternMap = new Dictionary(); private static void LoadProcessFeatureMap() { if (File.Exists("Features.xml")) { var doc = XDocument.Load("Features.xml"); var mappings = from item in doc.Descendants("DirectorySearch") select new { Name = item.Attribute("Name").Value, Suffix = item.Attribute("Suffix").Value }; foreach (var m in mappings) _featureMap[m.Name] = m.Suffix; var patterns = from item in doc.Descendants("FeaturePattern") select new { Pattern = item.Attribute("Pattern").Value, Feature = item.Attribute("Feature").Value }; foreach (var m in patterns) _featurePatternMap[new Regex(m.Pattern)] = m.Feature; var overrides = from item in doc.Descendants("IdentifierOverride") select new { Pattern = item.Attribute("Pattern").Value, Identifier = item.Attribute("Identifier").Value }; foreach (var o in overrides) _identifierOverrides[o.Pattern] = o.Identifier; } } /// /// Performs a search through the given XDocument for Directory fragments /// whose name matches the specified items in the FeatureMap, for each matching /// directory, all of its child elements will be assigned to the specified feature /// /// private static void ProcessFeatureMap(XDocument doc, string name) { Dictionary> componentMaps = new Dictionary>(); foreach (var dirName in _featureMap.Keys) { var suffix = _featureMap[dirName]; if (!componentMaps.ContainsKey(suffix)) componentMaps[suffix] = new HashSet(); Console.WriteLine("Checking if any Directory elements match: " + dirName); var matches = doc.Descendants("{" + nsWiX3 + "}Directory").Where(x => x.Attribute("Name").Value == dirName); foreach (var dir in matches) { Console.WriteLine("\tProcessing Directory (Id=" + dir.Attribute("Id") + ")"); foreach (var comp in dir.Descendants("{" + nsWiX3 + "}Component")) { //Console.WriteLine("\t\tSet Component Feature (Id=" + comp.Attribute("Id") + ") to use feature (" + suffix + ")"); componentMaps[suffix].Add(comp.Attribute("Id").Value); } } } //Remove these collected ids from the existing component group var currentGroup = doc.Descendants("{" + nsWiX3 + "}ComponentGroup").First(); var remove = new List(); int moved = 0; //Now write out the component group foreach(var suffix in _featureMap.Values) { if (componentMaps[suffix].Count > 0) { var groupId = name + suffix; var grp = new XElement("{" + nsWiX3 + "}ComponentGroup"); grp.SetAttributeValue("Id", groupId); foreach (var cid in componentMaps[suffix]) { var matches = currentGroup.Descendants("{" + nsWiX3 + "}ComponentRef").Where(x => x.Attribute("Id").Value == cid); remove.AddRange(matches); var el = new XElement("{" + nsWiX3 + "}ComponentRef"); el.SetAttributeValue("Id", cid); grp.Add(el); moved++; } //Prepend component group to the fragment doc.Descendants("{" + nsWiX3 + "}Fragment").First().AddFirst(grp); } } //Remove moved componentrefs from original group foreach (var el in remove) { el.Remove(); } if (moved > 0 || remove.Count > 0) Console.WriteLine("Moved {0} elements to new ComponentGroup. Removed {1} elements from original ComponentGroup", moved, remove.Count); } private static void ProcessFeaturePatternMap(XDocument doc) { Dictionary> componentMaps = new Dictionary>(); foreach (var pattern in _featurePatternMap.Keys) { var featureName = _featurePatternMap[pattern]; if (!componentMaps.ContainsKey(featureName)) componentMaps[featureName] = new HashSet(); //Console.WriteLine("Checking if any elements match: " + pattern); var directories = doc.Descendants("{" + nsWiX3 + "}Directory"); foreach (var dir in directories) { //Console.WriteLine("\tProcessing Directory (Id=" + dir.Attribute("Id") + ")"); foreach (var comp in dir.Descendants("{" + nsWiX3 + "}Component")) { var fileDesc = comp.Descendants("{" + nsWiX3 + "}File").FirstOrDefault(); if (fileDesc != null) { string source = fileDesc.Attribute("Source").Value; var matches = pattern.Matches(source); if (matches.Count >= 1) { //Console.WriteLine("{0} matched {1}", source, pattern); Console.WriteLine("Set Component Feature (" + comp.Attribute("Id") + ") to use feature (" + featureName + ")"); componentMaps[featureName].Add(comp.Attribute("Id").Value); comp.SetAttributeValue("Feature", featureName); } } } } } //Remove these collected ids from the existing component group var currentGroup = doc.Descendants("{" + nsWiX3 + "}ComponentGroup").First(); var remove = new List(); int removed = 0; //Remove these components from the component grup foreach (var pattern in _featurePatternMap.Keys) { var featureName = _featurePatternMap[pattern]; if (componentMaps[featureName].Count > 0) { foreach (var cid in componentMaps[featureName]) { //Console.WriteLine("Removing component id {0} from ComponentGroup", cid); var matches = currentGroup.Descendants("{" + nsWiX3 + "}ComponentRef").Where(x => x.Attribute("Id").Value == cid).ToArray(); foreach (var r in matches) { r.Remove(); removed++; } } } } if (removed > 0) Console.WriteLine("{0} elements detached from ComponentGroup", removed); //Console.WriteLine("Moved {0} elements to new ComponentGroup. Removed {1} elements from original ComponentGroup", moved, remove.Count); } /// /// Takes an existing .WXS file and generates an updated version, which /// is saved to a .PARAFFIN extension. /// /// /// 0 - The .PARAFFIN file was created. /// 2 - The input file does not have the special comment in the /// appropriate location. /// private static int UpdateExistingFile ( ) { int returnValue = 0; // Load the XML document. Any loading problems go right // to an exception for the user. XDocument inputDoc = XDocument.Load ( argValues.FileName ); XAttribute ns = inputDoc.Root.Attribute ( "xmlns" ); if ( 0 == String.Compare ( nsWiX2.ToString ( ) , ns.Value , StringComparison.OrdinalIgnoreCase ) ) { inputNameSpace = nsWiX2; } // The output filename. String outputFile = Path.ChangeExtension ( argValues.FileName , ".PARAFFIN" ); // The first node has to be comment I put there when // the file was created. XComment options = inputDoc.Root.FirstNode as XComment; if ( null != options ) { // It's a comment node, so set all the arguments from that // section. InitializeArgumentsFromFile ( options.Value ); // Create the new output file. XDocument outputDoc = new XDocument ( ); // Add the WiX and Fragment nodes. XElement outputRoot = new XElement ( nsWiX3 + "Wix" ); outputDoc.Add ( outputRoot ); XElement outputFragment = new XElement ( nsWiX3 + "Fragment" ); outputRoot.Add ( outputFragment ); // Find the directory ref of the input file. XElement inputDirRef = inputDoc.Descendants ( inputNameSpace + "DirectoryRef" ).First ( ); String idValue = inputDirRef.Attributes ( "Id" ).First ( ).Value; // Build a DirectoryRef for the output file. XElement outputDirRef = new XElement ( nsWiX3 + "DirectoryRef" , new XAttribute ( "Id" , idValue ) ); // Add the directory ref to the output file. outputFragment.Add ( outputDirRef ); // Get the starting directory values ready to go. InitializeDirectoryValues ( ); // Recurse through the input file and the directories // themselves. RecurseDirectoriesForExistingFile ( inputDirRef , outputDirRef , fullStartDirectory ); // Add the Component group node. AddComponentGroup ( outputFragment ); // Add the comment with all the command line options. AddCommandLineOptionsComment ( outputRoot ); ProcessFeatureMap(outputDoc, Path.GetFileNameWithoutExtension(outputFile)); ProcessFeaturePatternMap(outputDoc); // All OK, Jumpmaster! outputDoc.Save ( outputFile ); } else { // This does not look like a file this tool previously // generated. errorMessage = Constants.UnknownFileType; returnValue = 2; } return ( returnValue ); } /// /// Does the work of recursing both the file system directories and the /// original .WXS file to produce an updated XML document. /// /// /// The current element in the input .WXS file. /// /// /// The current element in the output .PARAFFIN file. /// /// /// The directory to process. This has to be the full directory value. /// /// /// As you can guess, this is called recursively. /// private static void RecurseDirectoriesForExistingFile ( XElement currInputElement , XElement currOutputElement , String directory ) { // If the currInputElement is null, I'm processing a brand new // directory that isn't in the original file. Thus, I can treat // adding this directory just like it's a new file and add this // directory, plus all under it. if ( null == currInputElement ) { RecurseDirectoriesForNewFile ( currOutputElement , directory ); } else { // The directory element I'm going to be building up. XElement outputDirElement; // Get the directory info in order to get just the name. DirectoryInfo info = new DirectoryInfo ( directory ); String name = info.Name; String matchAttrib = "Name"; // If this is a WiX 2.0 input file, I need to match on LongName // instead if the name is more than 8 characters. if ( ( inputNameSpace == nsWiX2 ) && ( name.Length > 8 ) ) { matchAttrib = "LongName"; } // Does this directory already exist in the input file? var qFindDirectory = from elem in currInputElement.Elements ( ) where ( (string)elem.Attribute ( matchAttrib ) == name ) select elem; XElement inputDirElement = null; int fileCount = qFindDirectory.Count ( ); Debug.Assert ( fileCount <= 1 , "fileCount <= 1" ); if ( fileCount > 1 ) { // We've got a serious problem. :( You can't have multiple // directories with the same name. String err = String.Format ( CultureInfo.CurrentCulture , Constants.InvalidFileNameCountFmt , name ); throw new InvalidOperationException ( err ); } else if ( 0 == fileCount ) { // This is a new directory. outputDirElement = CreateDirectoryElement ( directory ); } else { // We've got one element so grab it. inputDirElement = qFindDirectory.First ( ); if ( inputNameSpace == nsWiX2 ) { // In case the input file was created with the // version of Paraffin that supported WiX 2.0, I // want to strip off the LongName attribute // as it is no longer needed. inputDirElement.SetAttributeValue ( "LongName" , null ); // Since WiX3 uses the Name attribute, I willl change // the existing Name attribute to the long name so // it gets copied below. This is the long name now. inputDirElement.SetAttributeValue ( "Name" , name ); } // This directory was in the previous file so copy it's // attributes over to the new file. outputDirElement = new XElement ( nsWiX3 + "Directory" ); foreach ( var attrib in inputDirElement.Attributes ( ) ) { outputDirElement.SetAttributeValue ( attrib.Name , attrib.Value ); } } // Add this element to the output element. currOutputElement.Add ( outputDirElement ); // Process all the files in this directory as compared to the // input file. UpdateFilesInDirectoryNode ( directory , inputDirElement , outputDirElement ); // Recurse directories if the original file had that set. if ( false == argValues.NoDirectoryRecursion ) { String [] dirs = Directory.GetDirectories ( directory ); foreach ( var item in dirs ) { // Is this a directory the user wanted to skip? Boolean skipDirectory = IsDirectoryExcluded ( item ); if ( false == skipDirectory ) { RecurseDirectoriesForExistingFile ( inputDirElement , outputDirElement , item ); } } } } } /// /// Looks at the input .WXS for the files in this directory and compare /// it to the files on disk. If the file is the same or is a new file, /// add the files to the output. If it's no longer present on disk, but /// is in the input .WXS, skip adding the file to the output. /// /// The disk directory to scan. /// The Directory element from the .WXS file that /// maps to . /// The Directory element for the output /// .PARAFFIN file. /// /// Thrown if there's multiple files with the same name in the child elements. /// private static void UpdateFilesInDirectoryNode ( String directory , XElement inputDir , XElement outputDir ) { // If the inputDir element is null, just treat this as if I was // creating a new file. if ( null == inputDir ) { AddNewFilesToDirectoryNode ( directory , outputDir ); } else { // The .WXS file had files for this directory so I need to // match them all up with the existing files. // Start by getting the files in this directory. String [] files = Directory.GetFiles ( directory ); // Skip all those that have extension the user does not want. var filesQuery = from file in files where false == argValues.ExtensionList. ContainsKey ( Path.GetExtension ( file ) .ToUpperInvariant ( ) ) select file; // If there's no files in this directory, there's nothing else // to do. if ( 0 != filesQuery.Count ( ) ) { // I'll add to the output .WXS file current Directory node // by default. XElement addToElement = outputDir; // Are we doing multiple files per component? if ( true == argValues.MultipleFilesPerComponent ) { // Copy over the one component node. If you just add // the node directly, that copies over all the child // nodes as well so I need duplicate just the component // node itself. XElement intputCompElem = inputDir.Element ( inputNameSpace + "Component" ); XElement outputCompElem = new XElement ( nsWiX3 + "Component" ); foreach ( var attrib in intputCompElem.Attributes ( ) ) { outputCompElem.SetAttributeValue ( attrib.Name , attrib.Value ); } // Add the Component node on. outputDir.Add ( outputCompElem ); // Point to the component where I'll be adding all the // file nodes. addToElement = outputCompElem; } // First get the child component(s) from this Directory. var comps = inputDir.Elements ( inputNameSpace + "Component" ); // Now get all the files from just these Component // elements. var inputFiles = comps.Descendants ( inputNameSpace + "File" ); // Loop through all the files on disk. foreach ( var file in filesQuery ) { // Holds the element I'm going to add to the current // output XML document. XElement newOutputElem; // Get the aliased value for this file. String aliasedName = BuildAliasedFilename ( file ); // See if I can find that file in the input .WXS file // by checking the aliased Source names. var inputFileQuery = from fileNode in inputFiles where ( ( (string)fileNode.Attribute ( "Source" ) ) == aliasedName ) select fileNode; int fileCount = inputFileQuery.Count ( ); Debug.Assert ( fileCount <= 1 , "fileCount <= 1" ); if ( 0 == fileCount ) { XElement comp = null; if ( false == argValues.MultipleFilesPerComponent ) { // Put this file element in it's own Component. // The component always has to be created first. // so that the component and file are using the // same unique number. comp = CreateComponentElement ( ); } // This is a new file that wasn't in the input .WXS // file so just add it. First create a new File // element. newOutputElem = CreateFileElement ( file ); // Did I create a component for this file? if ( null != comp ) { // Add the file to this component. comp.Add ( newOutputElem ); // Point at the component to add. newOutputElem = comp; } } else if ( 1 == fileCount ) { newOutputElem = inputFileQuery.First ( ); // In case the input file was created with the // version of Paraffin that supported WiX 2.0, I // want to strip off the Name and LongName // attributes as they are no longer needed. newOutputElem.SetAttributeValue ( "Name" , null ); newOutputElem.SetAttributeValue ( "LongName" , null ); // Is it one file per component? if ( false == argValues.MultipleFilesPerComponent ) { // If this is a WiX 3.0 .WXS file I can simply // cheat and make the newOutputElem actually // point to the Component element. That way I // just add the Component and the child File // element in one swoop. if ( inputNameSpace == nsWiX3 ) { newOutputElem = newOutputElem.Parent; } else { // This is a fragment from WiX 2.0 being // updated to 3.0. Because of the namespace // being different, I need to copy the // attributes manually. XElement comp = new XElement ( nsWiX3 + "Component" ); foreach ( var attrib in newOutputElem.Parent.Attributes ( ) ) { comp.SetAttributeValue ( attrib.Name , attrib.Value ); } // Create the WiX 3 version of this WiX 2 // file node. XElement newFile = CreateWiX3FileFromWix2File ( newOutputElem ); // Add the file to the component. comp.Add ( newFile ); // Set the output element to the component. newOutputElem = comp; } } else if ( inputNameSpace == nsWiX2 ) { // It's multiple files per component and this is // a WiX 2.0 input file. Copy the attributes // over manually to keep the namespaces // straight. Now newOutput points to the WiX 3.0 // element. newOutputElem = CreateWiX3FileFromWix2File ( newOutputElem ); } } else { // There's multiple files with the same name in this // particular node. That's bad. :( String err = String.Format ( CultureInfo.CurrentCulture , Constants.InvalidFileNameCountFmt , file ); throw new InvalidOperationException ( err ); } // Add the file element to the parent node. addToElement.Add ( newOutputElem ); } } } } /// /// Called when processing a new file or a directory that's wasn't seen /// in the existing .WXS when updating. /// /// /// The current element in the output XML document. /// /// /// The disk directory to recurse. /// private static void RecurseDirectoriesForNewFile ( XElement currElement , String directory ) { // It's new so create a Directory element. XElement directoryNode = CreateDirectoryElement ( directory ); // Add the current directory to the passed in element currElement.Add ( directoryNode ); // Add the files to this directory node. AddNewFilesToDirectoryNode ( directory , directoryNode ); // Recurse the directories if I'm supposed to do so. if ( false == argValues.NoDirectoryRecursion ) { String [] dirs = Directory.GetDirectories ( directory ); foreach ( var item in dirs ) { Boolean skipDirectory = IsDirectoryExcluded ( item ); if ( false == skipDirectory ) { RecurseDirectoriesForNewFile ( directoryNode , item ); } } } } /// /// For new directories when creating new files or when adding new /// directories when processing an existing .WXS, adds the files /// to the element. /// /// /// The directory to get the files from. /// /// /// The Director element to add the new Component/File elements to. /// private static void AddNewFilesToDirectoryNode ( String directory , XElement directoryElem ) { // Get the files in this directory. String [] files = Directory.GetFiles ( directory ); // Only do the work if there are some files in the directory. if ( files.Length > 0 ) { // Skip all those that have extensions the user does not want. var filesQuery = from file in files where false == argValues.ExtensionList. ContainsKey ( Path.GetExtension ( file ) .ToUpperInvariant ( ) ) select file; // Create the first Component element. Only add this node to the // directory node if the user wants multiple files per component // node. XElement currentComponent = CreateComponentElement ( ); if ( true == argValues.MultipleFilesPerComponent ) { directoryElem.Add ( currentComponent ); } // For each file on disk. foreach ( var file in filesQuery ) { // Create the File element and add it to the current // Component element. XElement fileElement = CreateFileElement ( file ); currentComponent.Add ( fileElement ); if ( false == argValues.MultipleFilesPerComponent ) { directoryElem.Add ( currentComponent ); currentComponent = CreateComponentElement ( ); } } // I'm done with this directory so bump up the component and // directory count if the user asked for that to happen. componentNumber += ( argValues.IncrementValue - 2 ); } } /// /// Initializes the with all the /// settings from the first comment block. Used when reading in a .WXS /// to compare to the files on the disk. /// /// /// The XML string to process. /// private static void InitializeArgumentsFromFile ( string inputXml ) { XElement options = XElement.Parse ( inputXml ); // Save off the settings from the command line. ParaffinArgParser originalArgs = argValues; // Start the arguments from the comment section. argValues = new ParaffinArgParser ( ); // Get all the easy values out. argValues.CustomValue = options.Descendants ( CUSTOMELEM ).First ( ).Value; argValues.Alias = options.Descendants ( ALIASELEM ).First ( ).Value; argValues.StartDirectory = options.Descendants ( DIRECTORYELEM ).First ( ).Value; argValues.IncrementValue = Convert.ToInt32 ( options.Descendants ( INCREMENTELEM ).First ( ).Value , CultureInfo.InvariantCulture ); argValues.GenerateGuids = Convert.ToBoolean ( options.Descendants ( GUIDSELEM ).First ( ).Value , CultureInfo.InvariantCulture ); argValues.MultipleFilesPerComponent = Convert.ToBoolean ( options.Descendants ( MULTIPLEELEM ).First ( ).Value , CultureInfo.InvariantCulture ); argValues.NoDirectoryRecursion = Convert.ToBoolean ( options.Descendants ( NORECURSELEM ).First ( ).Value , CultureInfo.InvariantCulture ); directoryNumber = Convert.ToInt32 ( options.Descendants ( NEXTDIRECTORYNUMELEM ).First ( ).Value , CultureInfo.InvariantCulture ); componentNumber = Convert.ToInt32 ( options.Descendants ( NEXTCOMPONENTNUMBER ).First ( ).Value , CultureInfo.InvariantCulture ); var extNode = options.Descendants ( EXTEXCLUDEELEM ); foreach ( var item in extNode.Descendants ( ) ) { argValues.ExtensionList.Add ( item.Value , true ); } var dirEx = options.Descendants ( DIREEXCLUDEELEM ); foreach ( var item in dirEx.Descendants ( ) ) { argValues.DirectoryExcludeList.Add ( item.Value ); } // After releasing 1.0, I've added a few command line options. // Since I don't want to break existing PARAFFIN generated files, // I'll not require the following options to be in existing files. // If they are cool, but no sense crashing out if they aren't. var win64Elems = options.Descendants ( WIN64ELEM ); if ( win64Elems.Count ( ) == 1 ) { // Grab the value. argValues.Win64 = Convert.ToBoolean ( win64Elems.First ( ).Value , CultureInfo.InvariantCulture ); } else { // Assume false. argValues.Win64 = false; } // Now that everything is read out of the original options block, // add in any additional -ext and -dirExclude options specified on // the command line. foreach ( var cmdLineExt in originalArgs.ExtensionList.Keys ) { if ( false == argValues.ExtensionList.ContainsKey ( cmdLineExt ) ) { argValues.ExtensionList.Add ( cmdLineExt , true ); } } foreach ( var dirExclude in originalArgs.DirectoryExcludeList ) { if ( false == argValues.DirectoryExcludeList.Contains ( dirExclude ) ) { argValues.DirectoryExcludeList.Add ( dirExclude ); } } } /// /// Adds the command line options as the first comment element under /// the WiX element. /// /// /// The WiX element to add to. /// private static void AddCommandLineOptionsComment ( XElement wixElement ) { // Create the XML data for the easy stuff. XElement initOptions = new XElement ( CMDLINEOPTIONSELEM , new XElement ( PRODUCEDBYELEM , Constants.CommentProducer ) , new XElement ( WARNINGELEM , Constants.CommentWarning ) , new XElement ( DATECREATE , DateTime.Now.ToString ( "g" , CultureInfo.CurrentCulture ) ) , new XElement ( DIRECTORYELEM , argValues.StartDirectory ) , new XElement ( CUSTOMELEM , argValues.CustomValue ) , new XElement ( ALIASELEM , argValues.Alias ) , new XElement ( INCREMENTELEM , argValues.IncrementValue ) , new XElement ( GUIDSELEM , argValues.GenerateGuids ) , new XElement ( WIN64ELEM , argValues.Win64 ) , new XElement ( MULTIPLEELEM , argValues.MultipleFilesPerComponent ) , new XElement ( NORECURSELEM , argValues.NoDirectoryRecursion ) ); // Add the file extension exclusions. XElement extList = new XElement ( EXTEXCLUDEELEM ); foreach ( var item in argValues.ExtensionList ) { extList.Add ( new XElement ( EXTELEM , item.Key ) ); } initOptions.Add ( extList ); // Add the directory exclusions. XElement dirExList = new XElement ( DIREEXCLUDEELEM ); foreach ( var item in argValues.DirectoryExcludeList ) { dirExList.Add ( new XElement ( DIREXT , item.ToString ( ) ) ); } initOptions.Add ( dirExList ); // Add the next directory number and component number so we don't // overwrite an existing value. XElement dirNum = new XElement ( NEXTDIRECTORYNUMELEM , directoryNumber ); initOptions.Add ( dirNum ); XElement compNum = new XElement ( NEXTCOMPONENTNUMBER , componentNumber ); initOptions.Add ( compNum ); // Add the XML comment. XComment comment = new XComment ( initOptions.ToString ( ) ); wixElement.AddFirst ( comment ); } /// /// Adds the ComponentGroup as the first child to the Fragment element. /// /// /// The Fragment element. /// private static void AddComponentGroup ( XElement fragment ) { // Grab all the Component elements and sort them by the ID // attribute. var compNodes = from node in fragment. Descendants ( nsWiX3 + "Component" ) select node; // Sort them in logical order because I'm a little bit obsessive. compNodes = compNodes.OrderBy ( n => (string)n.Attribute ( "Id" ).Value , new LogicalStringComparer ( ) ); StringBuilder sb = new StringBuilder ( 70 ); sb.AppendFormat ( "group_{0}" , argValues.CustomValue ); // Ensure that all invalid characters are stripped from the ID. String id = RemoveInvalidIdCharacters ( sb.ToString ( ) ); XElement groupNode = new XElement ( nsWiX3 + "ComponentGroup" , new XAttribute ( "Id" , id ) ); foreach ( var component in compNodes ) { XElement refNode = new XElement ( nsWiX3 + "ComponentRef" , new XAttribute ( "Id" , component.Attribute ( "Id" ).Value ) ); groupNode.Add ( refNode ); } // Add the group node as the first child of the outputFragment only // if there are some components. It's perfectly reasonable to have // a fragment made up of nothing but directories. if ( 0 != compNodes.Count ( ) ) { fragment.AddFirst ( groupNode ); } } /// /// Gets the directory values initialized so the code can handle aliases /// and the individual directories. /// private static void InitializeDirectoryValues ( ) { fullStartDirectory = Path.GetFullPath ( argValues.StartDirectory ); DirectoryInfo info = new DirectoryInfo ( fullStartDirectory ); baseDirectoryName = info.Name; } /// /// MSI only accepts IDs that are 72 characters long so I need to ensure /// that the strings I use are within that limit. /// /// /// The initial part of the string. /// /// /// The main part of the string. /// /// /// The unique value to append to this string. /// /// /// A unique string that is only 70 characters long and has all invalid /// characters stripped. /// private static String CreateSeventyCharIdString ( String start , String main , int uniqueId ) { const String FormatStr = "{0}_{1}_{2}"; const Int32 MaxLen = 70; String uniqueStr = String.Format ( CultureInfo.InvariantCulture , "{0}" , uniqueId ); StringBuilder sb = new StringBuilder ( 100 ); sb.AppendFormat ( FormatStr , start , main , uniqueStr ); if ( sb.Length > MaxLen ) { sb.Length = 0; int idLen = uniqueStr.Length; int startLen = start.Length; int len = Math.Min ( main.Length , MaxLen - ( idLen + startLen ) ); String sub = main.Substring ( 0 , len ); sb.AppendFormat ( FormatStr , start , sub , uniqueStr ); } // Turns out id strings in WiX cannot have dashes in them so // convert them to underscores. String retVal = RemoveInvalidIdCharacters ( sb.ToString ( ) ); return ( retVal ); } /// /// Looks through the list of directory exclusions and returns true /// if this directory is supposed to be excluded. /// /// /// The directory to check if it's got any excluded value in it. /// /// /// True - Supposed to exclude and skip. /// False - Process this directory. /// private static Boolean IsDirectoryExcluded ( String directory ) { Boolean skipDirectory = false; // If the user wanted to skip some directories, check to see // if this happens to be one. for ( int i = 0 ; i < argValues.DirectoryExcludeList.Count ; i++ ) { if ( true == directory.Contains ( argValues.DirectoryExcludeList [ i ] ) ) { skipDirectory = true; break; } } return ( skipDirectory ); } /// /// Returns a unique name for the Directory Id attribute. /// /// /// The full directory to process. /// /// /// A string that encapsulates the directory name with a unique value /// appended. /// private static String GenerateUniqueDirectoryIdName ( String directory ) { // To make the Id a bit easier to read, I'm going to use a naming // scheme of "dir__". That will put some uniqueness on // the names so they don't conflict across large installs. // Suffix the directory with \ to ensure I find the exact match // when looking the base directory. if ( false == directory.EndsWith ( "\\" , StringComparison.OrdinalIgnoreCase ) ) { directory += "\\"; } // Figure out where the base directory name is in this string and // create a unique name for the Id attribute. String exactBaseDirectory = String.Format ( CultureInfo.InvariantCulture , "\\{0}\\" , baseDirectoryName ); Int32 startBaseDir = directory.IndexOf ( exactBaseDirectory , StringComparison.OrdinalIgnoreCase ); // Get the real value and skip the preceding \\ used to find the // exact base. String dirIdString = directory.Substring ( startBaseDir + 1 ); if ( '\\' == dirIdString [ dirIdString.Length - 1 ] ) { dirIdString = dirIdString.Substring ( 0 , dirIdString.Length - 1 ); } dirIdString = dirIdString.Replace ( '\\' , '.' ); dirIdString = CreateSeventyCharIdString ( "dir" , dirIdString , directoryNumber ); // Since I've used this directoryNumber, time to bump it up. directoryNumber += argValues.IncrementValue; return ( dirIdString ); } /// /// Creates a new Directory element. /// /// /// The file directory for this element. /// /// /// A constructed . /// private static XElement CreateDirectoryElement ( String directory ) { // Each directory element needs a unique value. String uniqueDirID = GenerateUniqueDirectoryIdName ( directory ); // Get the long and short names for this directory. DirectoryInfo info = new DirectoryInfo ( directory ); // I've got enough to create the Directory node. XElement directoryNode = new XElement ( nsWiX3 + "Directory" , new XAttribute ( "Id" , uniqueDirID ) , new XAttribute ( "Name" , info.Name ) ); return ( directoryNode ); } /// /// Creates a File element for the file in . /// /// /// The full filename to process. /// /// /// A valid for the File element. /// private static XElement CreateFileElement ( String fileName ) { // Create a unique filename. In a one file per component run, this // will mean that the file and it's parent component will have the // same number. String fileId = string.Empty; foreach (String pattern in _identifierOverrides.Keys) { if (fileName.EndsWith(pattern)) { fileId = _identifierOverrides[pattern]; Console.WriteLine("Set identifier [{0}] to file [{1}]", fileId, fileName); break; } } if (String.IsNullOrEmpty(fileId)) fileId = CreateSeventyCharIdString("file", argValues.CustomValue, componentNumber - 1); // If the user wanted to group all the files into a component, I // need to bump up the componentNumber to keep everything straight. if ( true == argValues.MultipleFilesPerComponent ) { componentNumber++; } XElement file = new XElement ( nsWiX3 + "File" , new XAttribute ( "Id" , fileId ) ); if ( true == IsPEFile ( fileName ) ) { file.Add ( new XAttribute ( "Checksum" , "yes" ) ); } fileName = BuildAliasedFilename ( fileName ); file.Add ( new XAttribute ( "Source" , fileName ) ); return ( file ); } /// /// Creates a WiX 3 File Element from a WiX 2 File Element. /// /// /// The WiX 2.0 file element. /// /// /// The WiX 3.0 file element. /// private static XElement CreateWiX3FileFromWix2File ( XElement input ) { XElement newFile = new XElement ( nsWiX3 + "File" ); foreach ( var attrib in input.Attributes ( ) ) { newFile.SetAttributeValue ( attrib.Name , attrib.Value ); } // If it's a PE binary file, add on the Checksum attribute. if ( true == IsPEFile ( newFile.Attribute ( "Source" ).Value ) ) { newFile.SetAttributeValue ( "Checksum" , "yes" ); } return ( newFile ); } /// /// Creates a standard Component element. /// /// /// A newly created and unique Component elements. /// private static XElement CreateComponentElement ( ) { // Make sure the Id field is less than or equal to 70 characters. String componentId = CreateSeventyCharIdString ( "comp" , argValues.CustomValue , componentNumber ); // Increment since I just used that number. componentNumber++; String guidString = "PUT-GUID-HERE"; if ( true == argValues.GenerateGuids ) { Guid g = Guid.NewGuid ( ); guidString = g.ToString ( ).ToUpperInvariant ( ); } XElement comp = new XElement ( nsWiX3 + "Component" , new XAttribute ( "Id" , componentId ) , new XAttribute ( "DiskId" , "1" ) , new XAttribute ( "KeyPath" , "yes" ) , new XAttribute ( "Guid" , guidString ) ); // Does the user want the Win64 attribute? if ( true == argValues.Win64 ) { comp.SetAttributeValue ( "Win64" , "yes" ); } return ( comp ); } private static String BuildAliasedFilename ( String fileName ) { // Does the user want an alias for the base directory name? if ( false == String.IsNullOrEmpty ( argValues.Alias ) ) { fileName = fileName.Replace ( fullStartDirectory , argValues.Alias ); } return ( fileName ); } private static Boolean IsPEFile ( String fileName ) { String ext = Path.GetExtension ( fileName ); ext = ext.ToUpper ( CultureInfo.CurrentCulture ); for ( int i = 0 ; i < peFileExtension.Length ; i++ ) { if ( 0 == String.Compare ( ext , peFileExtension[i] , true , CultureInfo.CurrentCulture ) ) { return ( true ); } } return ( false ); } private static String RemoveInvalidIdCharacters ( String input ) { // WiX 2.0 does not document the actual valid characters in an Id // attribute. This is especially true in Directory elements. What // I'll do here is replace everything that's not in the range // [0-9a-zA-Z_] with underscores. While that might make some of the // Ids harder to read, I'm assured CANDLE.EXE will compile the // fragment. return ( Regex.Replace ( input , "[^0-9a-zA-Z_]" , "_" ) ); } private static class NativeMethods { private const int MaxPath = 255; [DllImport ( "shlwapi.dll" , CharSet = CharSet.Unicode , ExactSpelling = true )] internal static extern int StrCmpLogicalW ( String x , String y ); } /// /// Used to sort values in strings in logical order. /// private class LogicalStringComparer : IComparer { /// /// Calls the native logical string compare method. /// /// /// The first string to compare. /// /// /// The second string to compare. /// /// /// Zero if the strings are identical, 1 if /// is greater, -1 if /// is greater. /// public int Compare ( string x , string y ) { return ( NativeMethods.StrCmpLogicalW ( x , y ) ); } } } }