Laden...

WPF-Anwendungen zu einer Assembly mergen

Erstellt von MrSparkle vor 12 Jahren Letzter Beitrag vor 12 Jahren 3.163 Views
MrSparkle Themenstarter:in
5.658 Beiträge seit 2006
vor 12 Jahren
WPF-Anwendungen zu einer Assembly mergen

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?

  • Obfuscation: Man kann den Quellcode der gesamten Anwendung schützen, selbst die Namen der öffentlichen Klassen und Member können verschlüsselt werden, da man keinen Zugriff mehr von außen auf die Assembly benötigt. Bei einer Bibliothek müssen nur die öffentlichen Klassen und Member der Haupt-DLL unverschlüsselt bleiben.
  • Einfachere Distribution

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:

  • Eingebettete Resourcen in XAML-Dateien (z.B. Bilder) sollten immer relativ zur XAML-Datei referenziert werden. Also immer <Image Source="../MyImages/MyIcon.png" /> anstatt <Image Source="/MyLibrary;component/MyImages/MyIcon.png" />. Die Namespaces der URIs werden derzeit noch nicht angepaßt, aber das läßt sich noch implementieren.
  • Bei den referenzierten Namespaces in den XAML-Datei könnte es zu Problemen können, da die bisherige Anpassung sehr naiv ist. Es wird bisher einfach für alle gemergten Assemblys das ";assembly=MyAssemblyName" aus der Referenzierung gelöscht.
  • Die Ausgabedateien müssen immer gelöscht werden, bevor man ein Projekt erneut im gleichen Ordner erstellt.
  • Ansonsten halt bei Interesse einfach ausprobieren und Feedback geben 😃

Roadmap:

  • Einfaches CommandLine-Programm zur Verwendung in Post-Build-Anweisungen oder Batch-Dateien.
  • Am Ende könnte ein VisualStudio-Plugin entstehen, mit dem man seine Anwendungen mit einem Klick mergen und verschlüsseln kann.

	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