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
}
}