using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace Tranxition.BuildTasks { /// /// Custom MSBuild task which performs variable value substitutions on WiX source files /// public class WixVarSubstitution : Task { // This pattern should match the way WiX variables are defined, e.g. '' // Except it only matches on the Data portion of the ProcessingInstruction, so you get: 'SourceLocation = ".."' private const string PIDataRegExPattern = "^{0}\\s*=\\s*\"(?.*)\""; #region Members private Dictionary _substitutions = new Dictionary( StringComparer.OrdinalIgnoreCase ); #endregion #region Properties /// /// Full path of the WiX source file to process /// public string SourceFile { get; set; } /// /// Whether or not the task should return a failure result in the event /// no variables where identified in which match /// the variables definied in . /// public bool FailOnZeroMatches { get; set; } /// /// String containing XML of the form: /// /// /// ... /// /// ]]> /// public string VariableDefinitions { get; set; } #endregion #region Methods /// /// True if the operation was successful, otherwise false public override bool Execute() { if ( 0 == VariableDefinitions.Length ) { // Nothing to do - no reason to fail Log.LogWarning( "No variable definitions were provided." ); return true; } if ( !EnsureFileIsReadAndWriteReady() ) { Log.LogError( "Unable to ensure presence and writeability of SourceFile '{0}'.", SourceFile ); return false; } // Parse the provide XML string var root = XElement.Parse( VariableDefinitions, LoadOptions.SetBaseUri ); var ns = root.GetDefaultNamespace(); // Build up a dictionary of variables and replacement values Array.ForEach( root.Elements( ns + "VariableDefinition" ).ToArray(), n => { _substitutions.Add( n.Attribute( "Name" ).Value, n.Attribute( "NewValue" ).Value ); } ); // Better dig into "LINQ to XML", RegEx, and Composite Formatting if you want to make any sense of this section try { // Aggregate _substitutions keys and produce a string like "(key1)|(key2)|(key3)..." var varNames = "(" + ( ( _substitutions.Keys.Aggregate( ( leftKey, rightKey ) => "(" + leftKey + ")|(" + rightKey ) + ")" ).TrimStart( '(' ) ); // Because of the way PIDataRegExPattern is laid out, just stuff the aggregated string into the pattern var varNamesRegEx = new Regex( string.Format( CultureInfo.InvariantCulture, PIDataRegExPattern, varNames ), RegexOptions.IgnoreCase ); // This query loads the SourceFile as an XDocument and retrieves all XProcessingInstruction nodes // whose Target is 'define' and whose name matches any of the _substitutions keys var replaceNodesQuery = from e in XDocument.Load( SourceFile, LoadOptions.PreserveWhitespace ).DescendantNodes() let n = e as XProcessingInstruction where ( e.NodeType == XmlNodeType.ProcessingInstruction ) && n != null // Not sure if I really need this or if it's even guaranteed to be checked first && n.Target == "define" && varNamesRegEx.IsMatch( n.Data ) select n; // Force deferred execution to complete so we can modify the XDocument var replaceNodes = replaceNodesQuery.ToArray(); // If there weren't any matching variables in the file exit, and follow guidance for result if ( 0 == replaceNodes.Length ) { if ( FailOnZeroMatches ) { Log.LogError( "No variables in SourceFile '{0}' matched the provided VariableDefinitions", SourceFile ); return true; } else { Log.LogWarning( "No variables in SourceFile '{0}' matched the provided VariableDefinitions", SourceFile ); return false; } } // Loop through each substitution with a nested loop against replaceable nodes and do a regex replacement // This is safe because the pattern match will only succeed when substition.Key matches the variable name // Note: It just assigns the Data property back to itself when there's no match foreach ( var substitution in _substitutions ) { Log.LogMessage( MessageImportance.Low, "> Attempting to replace variable '{0}' with value '{1}' in SourceFile '{2}'", substitution.Key, substitution.Value, SourceFile ); foreach ( var replaceNode in replaceNodes ) { var pattern = string.Format( CultureInfo.InvariantCulture, PIDataRegExPattern, substitution.Key ); var replacement = string.Format( CultureInfo.InvariantCulture, "{0} = \"{1}\"", substitution.Key, substitution.Value ); replaceNode.Data = Regex.Replace( replaceNode.Data, pattern, replacement, RegexOptions.IgnoreCase ); } } // Write the changes back out to the input file replaceNodes[ 0 ].Document.Save( SourceFile, SaveOptions.DisableFormatting ); Log.LogMessage( MessageImportance.Low, "> Exiting with Success" ); return true; } catch ( Exception ex ) { Log.LogErrorFromException( ex ); return false; } } /// /// Primary need is to make sure the file is not ReadOnly, but /// it makes sense to also ensure there's not anything else going on /// (encryption, etc) which might get in the way of processing it. /// /// True if is ; otherwise false private bool EnsureFileIsReadAndWriteReady() { if ( !File.Exists( SourceFile ) ) { Log.LogWarning( "Unable to locate SourceFile" ); return false; } var fi = new FileInfo(SourceFile); if ( fi.Attributes != FileAttributes.Normal ) { Log.LogMessage( "Setting SourceFile attributes to Normal" ); fi.Attributes = FileAttributes.Normal; if ( fi.Attributes != FileAttributes.Normal ) { Log.LogWarning( "Unable to set SourceFile attributes to Normal" ); return false; } } return true; } #endregion } }