Hallo allerseits,
bekanntlich ist es ja unmöglich, eine WPF-Anwendung oder -Bibliothek mit ILMerge in eine Assembly zu mergen.
Der von Microsoft vorgeschlagene Weg, die Unter-Assemblys in die Haupt-Assembly als Resourcen einzubetten, hat sich auch nicht als zufriedenstellend erwiesen, weil man dadurch verhindert, daß (a) die Assembly im Ganzen obfusziert werden kann und (b) dadurch in einigen Fällen Ringverweise entstehen können, wenn sich die einzelnen Assemblys auch gegenseitig referenzieren.
Was spricht dafür, eine Anwendung in eine Assembly zu mergen?
Warum kann man WPF-Assemblies nicht mergen?
Laut Microsoft liegt es an der Zuordnung der Namespaces zu den XAML-Dateien. Die Assemblies lassen sich zwar mergen, aber man bekommt zur Laufzeit eine Exception, sobald das erste Mal auf eine XAML-Datei zugegriffen wird.
Was kann man machen?
Der einfachste Weg ist es, die einzelnen Projekte und Projektdateien zu mergen, statt der binäre Ausgabe. Man führt also den Quellcode zusammen, nicht den IL-Code.
Wie funktioniert es?
Ich habe eine kleine Klasse geschrieben, die das Prinzip veranschaulicht. Die MergeProjects-Methode bekommt die Pfade des Ausgangsprojektes und des Zielprojektes übergeben. Das Ausgangsprojekt wird mit allen abhängigen Projekten gemeinsam in das Zielprojekt gemerged. Dazu wird die Projektmappen-Datei (*.csproj) geparst und eine neue mit den gleichen Einstellungen erzeugt. Alle Quellcode-, Resourcen- und XAML-Dateien werden in den Ordner des Zielprojektes kopiert, da die z.T. noch angepaßt werden müssen.
Am Ende hat man ein neues Projekt, daß man mit einem Skript per MSBuild kompilieren und dann bei Bedarf obfuszieren kann. Man kann das Ausgabeprojekt aber auch in VisualStudio öffnen und debuggen. Das ganze ist auch erfreulich schnell und hat bei meinen Projekten bisher ganz gut funktioniert.
Known Issues:
Roadmap:
using System.IO;
using Path = System.IO.Path;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
public static class ProjectMerger
{
/// <summary>
/// Used to create projects with.
/// </summary>
private static readonly ProjectCollection projectCollection = new ProjectCollection();
/// <summary>
/// Loads a project file (*.csproj) from file and merges it together with all dependend
/// projects into a new project on disk. All files will be copyied in the merged project's
/// folder. In XAML files (*.xaml) all namespaces of the merged projects will be adjusted.
/// </summary>
/// <param name="projectToMergePath">The path of the project file to merge.</param>
/// <param name="mergedProjectPath">The path of the project to merge all files into.</param>
public static void MergeProjects(string projectToMergePath, string mergedProjectPath)
{
Project project = projectCollection.LoadProject(projectToMergePath);
string destPath = Path.GetDirectoryName(mergedProjectPath);
ConvertProjectItems(project, destPath);
using (StreamWriter writer = new StreamWriter(mergedProjectPath))
project.Save(writer);
projectCollection.UnloadAllProjects();
}
/// <summary>
/// Converts all needed paths and namespaces of the original project's items,
/// saving them to a new project, and create a physical copy on the file system.
/// </summary>
/// <param name="projectToMerge">The project to merge.</param>
/// <param name="mergedProjectPath">The path of the merged project file.</param>
private static void ConvertProjectItems(Project projectToMerge, string mergedProjectPath)
{
HashSet<string> processedSubProjects = new HashSet<string>();
Queue<Tuple<Project, ProjectItem>> items = new Queue<Tuple<Project, ProjectItem>>(projectToMerge.Items.Count);
foreach (ProjectItem item in projectToMerge.Items)
items.Enqueue(new Tuple<Project, ProjectItem>(projectToMerge, item));
while (items.Count != 0)
{
Tuple<Project, ProjectItem> tuple = items.Dequeue();
Project project = tuple.Item1;
ProjectItem item = tuple.Item2;
string projectFolder = project.DirectoryPath;
switch (item.ItemType)
{
case "ProjectReference": // References projects
// Copy all elements to main project instead of creating ProjectReference item
string originalProjectPath = MakeAbsolute(item.EvaluatedInclude, projectFolder).LocalPath;
item.UnevaluatedInclude = ConvertToTargetPath(project, item.EvaluatedInclude, projectFolder, mergedProjectPath);
// Remove Reference from main project
if (project == projectToMerge)
projectToMerge.RemoveItem(item);
// Only copy file on first reference found
if (!processedSubProjects.Contains(originalProjectPath))
{
// Enqueue all items of referenced project and handle them later
Project subProject = projectCollection.LoadProject(originalProjectPath);
foreach (ProjectItem subProjectItem in subProject.Items)
items.Enqueue(new Tuple<Project, ProjectItem>(subProject, subProjectItem));
processedSubProjects.Add(originalProjectPath);
}
break;
case "Reference": // Referenced assemblies
// Convert path to referenced assembly
foreach (ProjectMetadata metadata in item.Metadata)
if (metadata.Name == "HintPath")
metadata.UnevaluatedValue = MakeAbsolute(metadata.EvaluatedValue, projectFolder).LocalPath;
// Include file into main project
if (project != projectToMerge)
AddItem(projectToMerge, item);
break;
case "Compile": // Code file
// Files to skip
if (Path.GetFileName(item.EvaluatedInclude) == "AssemblyInfo.cs" && (project != projectToMerge))
break;
// Copy file
item.UnevaluatedInclude = CopyFile(project, item.EvaluatedInclude, projectFolder, mergedProjectPath);
// Include file into main project
if (project != projectToMerge)
AddItem(projectToMerge, item);
break;
case "Page": // XAML
// Copy file and convert XAML-Namespaces
item.UnevaluatedInclude = CopyXamlFile(project, item.EvaluatedInclude, projectFolder, mergedProjectPath, processedSubProjects);
// Include file into main project
if (project != projectToMerge)
AddItem(projectToMerge, item);
break;
case "Resource": // Images
case "EmbeddedResource": // Resource files (*.resx)
case "None": // Settings files (*.settings; app.config)
// Copy file
item.UnevaluatedInclude = CopyFile(project, item.EvaluatedInclude, projectFolder, mergedProjectPath);
// Include file into main project
if (project != projectToMerge)
AddItem(projectToMerge, item);
break;
default:
break;
}
}
}
/// <summary>
/// Returns the path of a file relative to the merged project.
/// </summary>
/// <param name="project">The source project that is beeing merged.</param>
/// <param name="relativePath">The path of the file relative to the source project's folder.</param>
/// <param name="originalProjectFolder">The source project's folder.</param>
/// <param name="targetProjectFolder">The folder of the merged project.</param>
private static string ConvertToTargetPath(Project project, string relativePath, string originalProjectFolder, string targetProjectFolder)
{
string projectPath = Path.GetFileNameWithoutExtension(project.FullPath);
targetProjectFolder = Path.Combine(targetProjectFolder, projectPath);
string targetPath = MakeAbsolute(relativePath, EnsureFinalDirectorySeparator(targetProjectFolder)).LocalPath;
return targetPath;
}
/// <summary>
/// Copies a XAML file from a source project to the merged project by adjusting all namespaces of the merged projects.
/// </summary>
/// <param name="project">The source project that is beeing merged.</param>
/// <param name="relativePath">The path of the file relative to the source project's folder.</param>
/// <param name="originalProjectFolder">The source project's folder.</param>
/// <param name="targetProjectFolder">The folder of the merged project.</param>
/// <param name="mergedAssemblies">The paths of the assemblies that are beeing merged into the
/// project, used to adjust namespaces in the XAML file.</param>
/// <returns>Returns the path of the duplicated file.</returns>
private static string CopyXamlFile(Project project, string relativePath, string originalProjectFolder, string targetProjectFolder, HashSet<string> mergedAssemblies)
{
string originalPath = MakeAbsolute(relativePath, EnsureFinalDirectorySeparator(originalProjectFolder)).LocalPath;
string targetPath = ConvertToTargetPath(project, relativePath, originalProjectFolder, targetProjectFolder);
string markup = File.ReadAllText(originalPath);
foreach (string assemblyPath in mergedAssemblies)
{
string assemblyName = projectCollection.LoadProject(assemblyPath).GetProperty("AssemblyName").EvaluatedValue;
string removeNameSpace = string.Format(";assembly={0}", assemblyName);
markup = markup.Replace(removeNameSpace, string.Empty);
}
CreateFolders(targetPath); // Ensure directory exists
File.WriteAllText(targetPath, markup);
return targetPath;
}
/// <summary>
/// Copies a file from a source project to the merged project.
/// </summary>
/// <param name="project">The source project that is beeing merged.</param>
/// <param name="relativePath">The path of the file relative to the source project's folder.</param>
/// <param name="originalProjectFolder">The source project's folder.</param>
/// <param name="targetProjectFolder">The folder of the merged project.</param>
/// <returns>Returns the path of the duplicated file.</returns>
private static string CopyFile(Project project, string relativePath, string originalProjectFolder, string targetProjectFolder)
{
string originalPath = MakeAbsolute(relativePath, EnsureFinalDirectorySeparator(originalProjectFolder)).LocalPath;
string targetPath = ConvertToTargetPath(project, relativePath, originalProjectFolder, targetProjectFolder);
CopyFile(originalPath, targetPath);
return targetPath;
}
/// <summary>
/// Creates all folders needed to copy a file to the position specified by <paramref name="filename"/>.
/// </summary>
/// <param name="filename">The file that is beeing copied.</param>
private static void CreateFolders(string filename)
{
string folder = Path.GetDirectoryName(filename);
string file = Path.GetFileName(filename);
string baseFolder = folder;
Stack<string> foldersToCreate = new Stack<string>();
while (!Directory.Exists(baseFolder))
{
string parent = Path.GetDirectoryName(baseFolder);
foldersToCreate.Push(baseFolder.Substring(parent.Length + 1));
baseFolder = parent;
}
while (foldersToCreate.Count != 0)
{
string newFolder = Path.Combine(baseFolder, foldersToCreate.Pop());
Directory.CreateDirectory(newFolder);
baseFolder = newFolder;
}
}
/// <summary>
/// Copys a file from a source to a destination path.
/// </summary>
/// <param name="source">The path to the source file.</param>
/// <param name="dest">The file's destination path.</param>
private static void CopyFile(string source, string dest)
{
CreateFolders(dest); // Ensure directory exists
File.Copy(source, dest, true);
}
/// <summary>
/// Adds a <see cref="ProjectItem"/> to a <see cref="Project"/> by making sure not to create any duplicates.
/// </summary>
/// <param name="project">The project.</param>
/// <param name="item">The property item.</param>
private static void AddItem(Project project, ProjectItem item)
{
if (!project.Items.Any(existingItem => existingItem.ItemType == item.ItemType && existingItem.EvaluatedInclude == item.EvaluatedInclude))
project.AddItem(item.ItemType, item.EvaluatedInclude, item.Metadata.Select(metaItem => new KeyValuePair<string, string>(metaItem.Name, metaItem.UnevaluatedValue)));
}
/// <summary>
/// Converts a relative into an absolute <see cref="Uri"/>.
/// </summary>
/// <param name="relativePath">The relative path to convert.</param>
/// <param name="basePath">The path the relative path is relative to.</param>
private static Uri MakeAbsolute(string relativePath, string basePath)
{
Uri relativeUri = new Uri(relativePath, UriKind.Relative);
Uri baseUri = new Uri(EnsureFinalDirectorySeparator(basePath), UriKind.Absolute);
Uri result = MakeAbsolute(relativeUri, baseUri);
return result;
}
/// <summary>
/// Converts a relative into an absolute <see cref="Uri"/>.
/// </summary>
/// <param name="relativeUri">The relative path to convert.</param>
/// <param name="basePathUri">The path the relative path is relative to.</param>
private static Uri MakeAbsolute(Uri relativeUri, Uri basePathUri)
{
Uri filePath = new Uri(basePathUri, relativeUri);
return filePath;
}
/// <summary>
/// Converts an absolute into a relative <see cref="Uri"/>.
/// </summary>
/// <param name="basePathUri">The base path.</param>
/// <param name="absoluteUri">The Uri to convert.</param>
private static Uri MakeRelative(Uri basePathUri, Uri absoluteUri)
{
Uri filePath = basePathUri.MakeRelativeUri(absoluteUri);
return filePath;
}
/// <summary>
/// Makes sure a path ends with a final "\". This is necessary in order
/// to achieve correct results when converting absolute to relative paths.
/// </summary>
/// <param name="relativePath">The path to fix.</param>
/// <returns>Returns the fixed path</returns>
private static string EnsureFinalDirectorySeparator(string relativePath)
{
if (relativePath[relativePath.Length - 1] != System.IO.Path.DirectorySeparatorChar)
return relativePath + System.IO.Path.DirectorySeparatorChar;
else
return relativePath;
}
}
Weeks of programming can save you hours of planning