ILRepack/Steps/XamlResourcePathPatcherStep.cs (117 lines of code) (raw):
//
// Copyright (c) 2015 Timotei Dolean
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
using Mono.Cecil;
using Mono.Cecil.Cil;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace ILRepacking.Steps
{
//TODO: Maybe we should fix this in *all* (xaml) files?
internal class XamlResourcePathPatcherStep : IRepackStep
{
private readonly ILogger _logger;
private readonly IRepackContext _repackContext;
private static readonly Regex VersionRegex = new Regex("v(.?\\d)+;", RegexOptions.IgnoreCase);
public XamlResourcePathPatcherStep(ILogger logger, IRepackContext repackContext)
{
_logger = logger;
_repackContext = repackContext;
}
public void Perform()
{
var types = _repackContext.TargetAssemblyDefinition.Modules.SelectMany(m => m.Types);
_logger.Verbose("Processing XAML resource paths ...");
foreach (var type in types)
{
PatchIComponentConnector(type);
PatchWpfToolkitVersionResourceDictionary(type);
}
}
private void PatchWpfToolkitVersionResourceDictionary(TypeDefinition type)
{
// Extended WPF toolkit has a nasty way of including the xamls in the generic.xaml
// Instead of a simple ResourceDictionary they use a custom one which hardcodes
// the assembly name and version:
// <core:VersionResourceDictionary AssemblyName="Xceed.Wpf.Toolkit" SourcePath="Calculator/Themes/Generic.xaml" />
if (!"Xceed.Wpf.Toolkit.Core.VersionResourceDictionary".Equals(type.FullName))
return;
var endInitMethod = type.Methods.FirstOrDefault(m =>
m.Name == "System.ComponentModel.ISupportInitialize.EndInit");
if (endInitMethod == null)
{
_logger.Warn("Could not find a proper 'EndInit' method for Xceed.Wpf.Toolkit to patch!");
return;
}
PatchWpfToolkitEndInitMethod(endInitMethod);
}
private void PatchIComponentConnector(TypeDefinition type)
{
if (!type.Interfaces.Any(t => t.InterfaceType.FullName == "System.Windows.Markup.IComponentConnector"))
return;
var initializeMethod = type.Methods.FirstOrDefault(m =>
m.Name == "InitializeComponent" && m.Parameters.Count == 0);
if (initializeMethod == null || !initializeMethod.HasBody)
return;
_logger.Verbose(" - Patching type " + type.FullName);
PatchMethod(initializeMethod);
}
private void PatchWpfToolkitEndInitMethod(MethodDefinition method)
{
const string ComponentPathString = "{0};v{1};component/{2}";
foreach (var stringInstruction in method.Body.Instructions.Where(i => i.OpCode == OpCodes.Ldstr))
{
if (!ComponentPathString.Equals(stringInstruction.Operand as string))
continue;
stringInstruction.Operand =
string.Format(
"/{0};component/Xceed.Wpf.Toolkit/{{2}}",
_repackContext.PrimaryAssemblyDefinition.Name.Name);
}
}
private void PatchMethod(MethodDefinition method)
{
foreach (var stringInstruction in method.Body.Instructions.Where(i => i.OpCode == OpCodes.Ldstr))
{
string path = stringInstruction.Operand as string;
if (string.IsNullOrEmpty(path))
continue;
var type = method.DeclaringType;
var originalScope = _repackContext.MappingHandler.GetOrigTypeScope<ModuleDefinition>(type);
stringInstruction.Operand = PatchPath(
path,
_repackContext.PrimaryAssemblyDefinition,
originalScope.Assembly,
_repackContext.OtherAssemblies);
}
}
internal static string PatchPath(
string path,
AssemblyDefinition primaryAssembly,
AssemblyDefinition sourceAssembly,
IList<AssemblyDefinition> otherAssemblies)
{
if (string.IsNullOrEmpty(path) || !(path.StartsWith("/") || path.StartsWith("pack://")))
return path;
string patchedPath = path;
if (primaryAssembly == sourceAssembly)
{
if (otherAssemblies.Any(assembly => TryPatchPath(path, primaryAssembly, assembly, out patchedPath)))
return patchedPath;
return path;
}
if (TryPatchPath(path, primaryAssembly, sourceAssembly, out patchedPath))
return patchedPath;
if (!path.EndsWith(".xaml"))
return path;
// we've got no non-primary assembly knowledge so far,
// that means it's a relative path in the source assembly -> just add the assembly's name as subdirectory
// /themes/file.xaml -> /library/themes/file.xaml
return "/" + sourceAssembly.Name.Name + path;
}
private static bool TryPatchPath(
string path, AssemblyDefinition primaryAssembly, AssemblyDefinition referenceAssembly, out string patchedPath)
{
// get rid of potential versions in the path
// Starting with a new .NET MSBuild version, in case the project is built
// via a new-format .csproj, the version is appended
path = VersionRegex.Replace(path, string.Empty);
string referenceAssemblyPath = GetAssemblyPath(referenceAssembly);
string newPath = GetAssemblyPath(primaryAssembly) + "/" + referenceAssembly.Name.Name;
// /library;component/file.xaml -> /primary;component/library/file.xaml
patchedPath = path.Replace(referenceAssemblyPath, newPath);
// if they're modified, we're good!
return !ReferenceEquals(patchedPath, path);
}
private static string GetAssemblyPath(AssemblyDefinition sourceAssembly)
{
return string.Format("/{0};component", sourceAssembly.Name.Name);
}
}
}