// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. namespace WixToolset.Harvesters { using System; using System.IO; using System.Reflection; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; using System.Xml; using WixToolset.Data; using WixToolset.Extensibility.Services; using WixToolset.Harvesters.Data; using WixToolset.Harvesters.Extensibility; using Wix = WixToolset.Harvesters.Serialize; /// /// Harvest WiX authoring for the outputs of a VS project. /// public sealed class VSProjectHarvester : BaseHarvesterExtension { // These format strings are used for generated element identifiers. // {0} = project name // {1} = POG name // {2} = file name private const string DirectoryIdFormat = "{0}.{1}"; private const string ComponentIdFormat = "{0}.{1}.{2}"; private const string FileIdFormat = "{0}.{1}.{2}"; private const string VariableFormat = "$(var.{0}.{1})"; private const string WixVariableFormat = "!(wix.{0}.{1})"; private const string ComponentPrefix = "cmp"; private const string DirectoryPrefix = "dir"; private const string FilePrefix = "fil"; private string projectGUID; private string directoryIds; private string directoryRefSeed; private string projectName; private string configuration; private string platform; private bool setUniqueIdentifiers; private GenerateType generateType; private bool generateWixVars; private static readonly ProjectOutputGroup[] allOutputGroups = new ProjectOutputGroup[] { new ProjectOutputGroup("Binaries", "BuiltProjectOutputGroup", "TargetDir"), new ProjectOutputGroup("Symbols", "DebugSymbolsProjectOutputGroup", "TargetDir"), new ProjectOutputGroup("Documents", "DocumentationProjectOutputGroup", "ProjectDir"), new ProjectOutputGroup("Satellites", "SatelliteDllsProjectOutputGroup", "TargetDir"), new ProjectOutputGroup("Sources", "SourceFilesProjectOutputGroup", "ProjectDir"), new ProjectOutputGroup("Content", "ContentFilesProjectOutputGroup", "ProjectDir"), }; private string[] outputGroups; /// /// Instantiate a new VSProjectHarvester. /// /// List of project output groups to harvest. public VSProjectHarvester(string[] outputGroups) { if (outputGroups == null) { throw new ArgumentNullException("outputGroups"); } this.outputGroups = outputGroups; } /// /// Gets or sets the configuration to set when harvesting. /// /// The configuration to set when harvesting. public string Configuration { get { return this.configuration; } set { this.configuration = value; } } public string DirectoryIds { get { return this.directoryIds; } set { this.directoryIds = value; } } /// /// Gets or sets what type of elements are to be generated. /// /// The type of elements being generated. public GenerateType GenerateType { get { return this.generateType; } set { this.generateType = value; } } /// /// Gets or sets whether or not to use wix variables. /// /// Whether or not to use wix variables. public bool GenerateWixVars { get { return this.generateWixVars; } set { this.generateWixVars = value; } } /// /// Gets or sets the location to load MSBuild from. /// public string MsbuildBinPath { get; set; } /// /// Gets or sets the platform to set when harvesting. /// /// The platform to set when harvesting. public string Platform { get { return this.platform; } set { this.platform = value; } } /// /// Gets or sets the project name to use in wix variables. /// /// The project name to use in wix variables. public string ProjectName { get { return this.projectName; } set { this.projectName = value; } } /// /// Gets or sets the option to set unique identifiers. /// /// The option to set unique identifiers. public bool SetUniqueIdentifiers { get { return this.setUniqueIdentifiers; } set { this.setUniqueIdentifiers = value; } } /// /// Gets or sets whether to ignore MsbuildBinPath when the project file specifies a known MSBuild version. /// public bool UseToolsVersion { get; set; } /// /// Gets a list of friendly output group names that will be recognized on the command-line. /// /// Array of output group names. public static string[] GetOutputGroupNames() { string[] names = new string[VSProjectHarvester.allOutputGroups.Length]; for (int i = 0; i < names.Length; i++) { names[i] = VSProjectHarvester.allOutputGroups[i].Name; } return names; } /// /// Harvest a VS project. /// /// The path of the VS project file. /// The harvested directory. public override Wix.Fragment[] Harvest(string argument) { if (null == argument) { throw new ArgumentNullException("argument"); } if (!System.IO.File.Exists(argument)) { throw new FileNotFoundException(argument); } // Match specified output group names to available POG structures // and collect list of build output groups to pass to MSBuild. ProjectOutputGroup[] pogs = new ProjectOutputGroup[this.outputGroups.Length]; string[] buildOutputGroups = new string[this.outputGroups.Length]; for (int i = 0; i < this.outputGroups.Length; i++) { foreach (ProjectOutputGroup pog in VSProjectHarvester.allOutputGroups) { if (pog.Name == this.outputGroups[i]) { pogs[i] = pog; buildOutputGroups[i] = pog.BuildOutputGroup; } } if (buildOutputGroups[i] == null) { throw new WixException(HarvesterErrors.InvalidOutputGroup(this.outputGroups[i])); } } string projectFile = Path.GetFullPath(argument); IDictionary buildOutputs = this.GetProjectBuildOutputs(projectFile, buildOutputGroups); ArrayList fragmentList = new ArrayList(); for (int i = 0; i < pogs.Length; i++) { this.HarvestProjectOutputGroup(projectFile, buildOutputs, pogs[i], fragmentList); } return (Wix.Fragment[]) fragmentList.ToArray(typeof(Wix.Fragment)); } /// /// Runs MSBuild on a project file to get the list of filenames for the specified output groups. /// /// VS MSBuild project file to load. /// List of MSBuild output group names. /// Dictionary mapping output group names to lists of filenames in the group. private IDictionary GetProjectBuildOutputs(string projectFile, string[] buildOutputGroups) { MSBuildProject project = this.GetMsbuildProject(projectFile); project.Load(projectFile); IDictionary buildOutputs = new Hashtable(); string originalDirectory = System.IO.Directory.GetCurrentDirectory(); System.IO.Directory.SetCurrentDirectory(Path.GetDirectoryName(projectFile)); bool buildSuccess = false; try { buildSuccess = project.Build(projectFile, buildOutputGroups, buildOutputs); } finally { System.IO.Directory.SetCurrentDirectory(originalDirectory); } if (!buildSuccess) { throw new WixException(HarvesterErrors.BuildFailed()); } this.projectGUID = project.GetEvaluatedProperty("ProjectGuid"); if (null == this.projectGUID) { throw new WixException(HarvesterErrors.BuildFailed()); } IDictionary newDictionary = new Dictionary(); foreach (string buildOutput in buildOutputs.Keys) { IEnumerable buildOutputFiles = buildOutputs[buildOutput] as IEnumerable; bool hasFiles = false; foreach (object file in buildOutputFiles) { hasFiles = true; break; } // Try the item group if no outputs if (!hasFiles) { IEnumerable itemFiles = project.GetEvaluatedItemsByName(String.Concat(buildOutput, "Output")); List itemFileList = new List(); // Get each BuildItem and add the file path to our list foreach (object itemFile in itemFiles) { itemFileList.Add(project.GetBuildItem(itemFile)); } // Use our list for this build output newDictionary.Add(buildOutput, itemFileList); } else { newDictionary.Add(buildOutput, buildOutputFiles); } } return newDictionary; } /// /// Creates WiX fragments for files in one output group. /// /// VS MSBuild project file. /// Dictionary of build outputs retrieved from an MSBuild run on the project file. /// Project output group parameters. /// List to which generated fragments will be added. /// Count of harvested files. private int HarvestProjectOutputGroup(string projectFile, IDictionary buildOutputs, ProjectOutputGroup pog, IList fragmentList) { string projectName = Path.GetFileNameWithoutExtension(projectFile); string projectBaseDir = null; if (this.ProjectName != null) { projectName = this.ProjectName; } string sanitizedProjectName = this.Core.CreateIdentifierFromFilename(projectName); Wix.IParentElement harvestParent; if (this.GenerateType == GenerateType.Container) { Wix.Container container = new Wix.Container(); harvestParent = container; container.Name = String.Format(CultureInfo.InvariantCulture, DirectoryIdFormat, sanitizedProjectName, pog.Name); } else if (this.GenerateType == GenerateType.PayloadGroup) { Wix.PayloadGroup payloadGroup = new Wix.PayloadGroup(); harvestParent = payloadGroup; payloadGroup.Id = String.Format(CultureInfo.InvariantCulture, DirectoryIdFormat, sanitizedProjectName, pog.Name); } else if (this.GenerateType == GenerateType.PackageGroup) { Wix.PackageGroup packageGroup = new Wix.PackageGroup(); harvestParent = packageGroup; packageGroup.Id = String.Format(CultureInfo.InvariantCulture, DirectoryIdFormat, sanitizedProjectName, pog.Name); } else { string directoryRefId; if (!String.IsNullOrEmpty(this.directoryIds)) { directoryRefId = this.directoryIds; } else if (this.setUniqueIdentifiers) { directoryRefId = String.Format(CultureInfo.InvariantCulture, DirectoryIdFormat, sanitizedProjectName, pog.Name); } else { directoryRefId = this.Core.CreateIdentifierFromFilename(String.Format(CultureInfo.InvariantCulture, DirectoryIdFormat, sanitizedProjectName, pog.Name)); } var directoryRef = DirectoryHelper.CreateDirectoryReference(directoryRefId); harvestParent = directoryRef; this.directoryRefSeed = this.Core.GenerateIdentifier(DirectoryPrefix, this.projectGUID, pog.Name); } IEnumerable pogFiles = buildOutputs[pog.BuildOutputGroup] as IEnumerable; if (pogFiles == null) { throw new WixException(HarvesterErrors.MissingProjectOutputGroup( projectFile, pog.BuildOutputGroup)); } if (pog.FileSource == "ProjectDir") { projectBaseDir = Path.GetDirectoryName(projectFile) + "\\"; } int harvestCount = this.HarvestProjectOutputGroupFiles(projectBaseDir, sanitizedProjectName, pog.Name, pog.FileSource, pogFiles, harvestParent); if (this.GenerateType == GenerateType.Container) { // harvestParent must be a Container at this point Wix.Container container = harvestParent as Wix.Container; Wix.Fragment fragment = new Wix.Fragment(); fragment.AddChild(container); fragmentList.Add(fragment); } else if (this.GenerateType == GenerateType.PackageGroup) { // harvestParent must be a PackageGroup at this point Wix.PackageGroup packageGroup = harvestParent as Wix.PackageGroup; Wix.Fragment fragment = new Wix.Fragment(); fragment.AddChild(packageGroup); fragmentList.Add(fragment); } else if (this.GenerateType == GenerateType.PayloadGroup) { // harvestParent must be a Container at this point Wix.PayloadGroup payloadGroup = harvestParent as Wix.PayloadGroup; Wix.Fragment fragment = new Wix.Fragment(); fragment.AddChild(payloadGroup); fragmentList.Add(fragment); } else { // harvestParent must be a DirectoryRef at this point Wix.DirectoryRef directoryRef = harvestParent as Wix.DirectoryRef; if (harvestCount > 0) { Wix.Fragment drf = new Wix.Fragment(); drf.AddChild(directoryRef); fragmentList.Add(drf); } Wix.ComponentGroup cg = new Wix.ComponentGroup(); if (this.setUniqueIdentifiers || !String.IsNullOrEmpty(this.directoryIds)) { cg.Id = String.Format(CultureInfo.InvariantCulture, DirectoryIdFormat, sanitizedProjectName, pog.Name); } else { cg.Id = directoryRef.Id; } if (harvestCount > 0) { this.AddComponentsToComponentGroup(directoryRef, cg); } Wix.Fragment cgf = new Wix.Fragment(); cgf.AddChild(cg); fragmentList.Add(cgf); } return harvestCount; } /// /// Add all Components in an element tree to a ComponentGroup. /// /// Parent of an element tree that will be searched for Components. /// The ComponentGroup the Components will be added to. private void AddComponentsToComponentGroup(Wix.IParentElement parent, Wix.ComponentGroup cg) { foreach (Wix.ISchemaElement childElement in parent.Children) { Wix.Component c = childElement as Wix.Component; if (c != null) { Wix.ComponentRef cr = new Wix.ComponentRef(); cr.Id = c.Id; cg.AddChild(cr); } else { Wix.IParentElement p = childElement as Wix.IParentElement; if (p != null) { this.AddComponentsToComponentGroup(p, cg); } } } } /// /// Harvest files from one output group of a VS project. /// /// The base directory of the files. /// Name of the project, to be used as a prefix for generated identifiers. /// Name of the project output group, used for generating identifiers for WiX elements. /// The ProjectOutputGroup file source. /// The files from one output group to harvest. /// The parent element that will contain the components of the harvested files. /// The number of files harvested. private int HarvestProjectOutputGroupFiles(string baseDir, string projectName, string pogName, string pogFileSource, IEnumerable outputGroupFiles, Wix.IParentElement parent) { int fileCount = 0; Wix.ISchemaElement exeFile = null; Wix.ISchemaElement dllFile = null; Wix.ISchemaElement appConfigFile = null; // Keep track of files inserted // Files can have different absolute paths but get mapped to the same SourceFile // after the project variables have been used. For example, a WiX project that // is building multiple cultures will have many output MSIs/MSMs, but will all get // mapped to $(var.ProjName.TargetDir)\ProjName.msm. These duplicates would // prevent generated code from compiling. Dictionary seenList = new Dictionary(); foreach (object output in outputGroupFiles) { string filePath = output.ToString(); string fileName = Path.GetFileName(filePath); string fileDir = Path.GetDirectoryName(filePath); string link = null; MethodInfo getMetadataMethod = output.GetType().GetMethod("GetMetadata"); if (getMetadataMethod != null) { link = (string)getMetadataMethod.Invoke(output, new object[] { "Link" }); if (!String.IsNullOrEmpty(link)) { fileDir = Path.GetDirectoryName(Path.Combine(baseDir, link)); } } Wix.IParentElement parentDir = parent; // Ignore Containers and PayloadGroups because they do not have a nested structure. if (baseDir != null && !String.Equals(Path.GetDirectoryName(baseDir), fileDir, StringComparison.OrdinalIgnoreCase) && this.GenerateType != GenerateType.Container && this.GenerateType != GenerateType.PackageGroup && this.GenerateType != GenerateType.PayloadGroup) { Uri baseUri = new Uri(baseDir); Uri relativeUri = baseUri.MakeRelativeUri(new Uri(fileDir)); parentDir = this.GetSubDirElement(parentDir, relativeUri); } string parentDirId = null; if (parentDir is Wix.DirectoryRef) { parentDirId = this.directoryRefSeed; } else if (parentDir is Wix.Directory) { parentDirId = ((Wix.Directory)parentDir).Id; } if (this.GenerateType == GenerateType.Container || this.GenerateType == GenerateType.PayloadGroup) { Wix.Payload payload = new Wix.Payload(); this.HarvestProjectOutputGroupPayloadFile(baseDir, projectName, pogName, pogFileSource, filePath, fileName, link, parentDir, payload, seenList); } else if (this.GenerateType == GenerateType.PackageGroup) { this.HarvestProjectOutputGroupPackage(projectName, pogName, pogFileSource, filePath, fileName, link, parentDir, seenList); } else { Wix.Component component = new Wix.Component(); Wix.File file = new Wix.File(); this.HarvestProjectOutputGroupFile(baseDir, projectName, pogName, pogFileSource, filePath, fileName, link, parentDir, parentDirId, component, file, seenList); if (String.Equals(Path.GetExtension(file.Source), ".exe", StringComparison.OrdinalIgnoreCase)) { exeFile = file; } else if (String.Equals(Path.GetExtension(file.Source), ".dll", StringComparison.OrdinalIgnoreCase)) { dllFile = file; } else if (file.Source.EndsWith("app.config", StringComparison.OrdinalIgnoreCase)) { appConfigFile = file; } } fileCount++; } // if there was no exe file found fallback on the dll file found if (exeFile == null && dllFile != null) { exeFile = dllFile; } // Special case for the app.config file in the Binaries POG... // The POG refers to the files in the OBJ directory, while the // generated WiX code references them in the bin directory. // The app.config file gets renamed to match the exe name. if ("Binaries" == pogName && null != exeFile && null != appConfigFile) { if (appConfigFile is Wix.File) { Wix.File appConfigFileAsWixFile = appConfigFile as Wix.File; Wix.File exeFileAsWixFile = exeFile as Wix.File; // Case insensitive replace appConfigFileAsWixFile.Source = Regex.Replace(appConfigFileAsWixFile.Source, @"app\.config", Path.GetFileName(exeFileAsWixFile.Source) + ".config", RegexOptions.IgnoreCase); } } return fileCount; } private void HarvestProjectOutputGroupFile(string baseDir, string projectName, string pogName, string pogFileSource, string filePath, string fileName, string link, Wix.IParentElement parentDir, string parentDirId, Wix.Component component, Wix.File file, Dictionary seenList) { string varFormat = VariableFormat; if (this.generateWixVars) { varFormat = WixVariableFormat; } if (pogName.Equals("Satellites", StringComparison.OrdinalIgnoreCase)) { Wix.Directory locDirectory = new Wix.Directory(); locDirectory.Name = Path.GetFileName(Path.GetDirectoryName(Path.GetFullPath(filePath))); file.Source = String.Concat(String.Format(CultureInfo.InvariantCulture, varFormat, projectName, pogFileSource), "\\", locDirectory.Name, "\\", Path.GetFileName(filePath)); if (!seenList.ContainsKey(file.Source)) { parentDir.AddChild(locDirectory); locDirectory.AddChild(component); component.AddChild(file); seenList.Add(file.Source, true); if (this.setUniqueIdentifiers) { locDirectory.Id = this.Core.GenerateIdentifier(DirectoryPrefix, parentDirId, locDirectory.Name); file.Id = this.Core.GenerateIdentifier(FilePrefix, locDirectory.Id, fileName); component.Id = this.Core.GenerateIdentifier(ComponentPrefix, locDirectory.Id, file.Id); } else { locDirectory.Id = this.Core.CreateIdentifierFromFilename(String.Format(DirectoryIdFormat, (parentDir is Wix.DirectoryRef) ? ((Wix.DirectoryRef)parentDir).Id : parentDirId, locDirectory.Name)); file.Id = this.Core.CreateIdentifierFromFilename(String.Format(CultureInfo.InvariantCulture, VSProjectHarvester.FileIdFormat, projectName, pogName, String.Concat(locDirectory.Name, ".", fileName))); component.Id = this.Core.CreateIdentifierFromFilename(String.Format(CultureInfo.InvariantCulture, VSProjectHarvester.ComponentIdFormat, projectName, pogName, String.Concat(locDirectory.Name, ".", fileName))); } } } else { file.Source = GenerateSourceFilePath(baseDir, projectName, pogFileSource, filePath, link, varFormat); if (!seenList.ContainsKey(file.Source)) { component.AddChild(file); parentDir.AddChild(component); seenList.Add(file.Source, true); if (this.setUniqueIdentifiers) { file.Id = this.Core.GenerateIdentifier(FilePrefix, parentDirId, fileName); component.Id = this.Core.GenerateIdentifier(ComponentPrefix, parentDirId, file.Id); } else { file.Id = this.Core.CreateIdentifierFromFilename(String.Format(CultureInfo.InvariantCulture, VSProjectHarvester.FileIdFormat, projectName, pogName, fileName)); component.Id = this.Core.CreateIdentifierFromFilename(String.Format(CultureInfo.InvariantCulture, VSProjectHarvester.ComponentIdFormat, projectName, pogName, fileName)); } } } } private void HarvestProjectOutputGroupPackage(string projectName, string pogName, string pogFileSource, string filePath, string fileName, string link, Wix.IParentElement parentDir, Dictionary seenList) { string varFormat = VariableFormat; if (this.generateWixVars) { varFormat = WixVariableFormat; } if (pogName.Equals("Binaries", StringComparison.OrdinalIgnoreCase)) { if (String.Equals(Path.GetExtension(filePath), ".exe", StringComparison.OrdinalIgnoreCase)) { Wix.ExePackage exePackage = new Wix.ExePackage(); exePackage.SourceFile = String.Concat(String.Format(CultureInfo.InvariantCulture, varFormat, projectName, pogFileSource), "\\", Path.GetFileName(filePath)); if (!seenList.ContainsKey(exePackage.SourceFile)) { parentDir.AddChild(exePackage); seenList.Add(exePackage.SourceFile, true); } } else if (String.Equals(Path.GetExtension(filePath), ".msi", StringComparison.OrdinalIgnoreCase)) { Wix.MsiPackage msiPackage = new Wix.MsiPackage(); msiPackage.SourceFile = String.Concat(String.Format(CultureInfo.InvariantCulture, varFormat, projectName, pogFileSource), "\\", Path.GetFileName(filePath)); if (!seenList.ContainsKey(msiPackage.SourceFile)) { parentDir.AddChild(msiPackage); seenList.Add(msiPackage.SourceFile, true); } } } } private void HarvestProjectOutputGroupPayloadFile(string baseDir, string projectName, string pogName, string pogFileSource, string filePath, string fileName, string link, Wix.IParentElement parentDir, Wix.Payload file, Dictionary seenList) { string varFormat = VariableFormat; if (this.generateWixVars) { varFormat = WixVariableFormat; } if (pogName.Equals("Satellites", StringComparison.OrdinalIgnoreCase)) { string locDirectoryName = Path.GetFileName(Path.GetDirectoryName(Path.GetFullPath(filePath))); file.SourceFile = String.Concat(String.Format(CultureInfo.InvariantCulture, varFormat, projectName, pogFileSource), "\\", locDirectoryName, "\\", Path.GetFileName(filePath)); if (!seenList.ContainsKey(file.SourceFile)) { parentDir.AddChild(file); seenList.Add(file.SourceFile, true); } } else { file.SourceFile = GenerateSourceFilePath(baseDir, projectName, pogFileSource, filePath, link, varFormat); if (!seenList.ContainsKey(file.SourceFile)) { parentDir.AddChild(file); seenList.Add(file.SourceFile, true); } } } /// /// Helper function to generates a source file path when harvesting files. /// /// /// /// /// /// /// /// private static string GenerateSourceFilePath(string baseDir, string projectName, string pogFileSource, string filePath, string link, string varFormat) { string ret; if (null == baseDir && !String.IsNullOrEmpty(link)) { // This needs to be the absolute path as a link can be located anywhere. ret = filePath; } else if (null == baseDir) { ret = String.Concat(String.Format(CultureInfo.InvariantCulture, varFormat, projectName, pogFileSource), "\\", Path.GetFileName(filePath)); } else if (filePath.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase)) { ret = String.Concat(String.Format(CultureInfo.InvariantCulture, varFormat, projectName, pogFileSource), "\\", filePath.Substring(baseDir.Length)); } else { // come up with a relative path to the file Uri sourcePathUri = new Uri(filePath); Uri baseDirUri = new Uri(baseDir); Uri sourceRelativeUri = baseDirUri.MakeRelativeUri(sourcePathUri); string relativePath = sourceRelativeUri.ToString().Replace('/', Path.DirectorySeparatorChar); if (!sourceRelativeUri.UserEscaped) { relativePath = Uri.UnescapeDataString(relativePath); } ret = String.Concat(String.Format(CultureInfo.InvariantCulture, varFormat, projectName, pogFileSource), "\\", relativePath); } return ret; } /// /// Gets a Directory element corresponding to a relative subdirectory within the project, /// either by locating a suitable existing Directory or creating a new one. /// /// The parent element which the subdirectory is relative to. /// Relative path of the subdirectory. /// Directory element for the relative path. private Wix.IParentElement GetSubDirElement(Wix.IParentElement parentDir, Uri relativeUri) { string[] segments = relativeUri.ToString().Split('\\', '/'); string firstSubDirName = Uri.UnescapeDataString(segments[0]); DirectoryAttributeAccessor subDir = null; if (String.Equals(firstSubDirName, "..", StringComparison.Ordinal)) { return parentDir; } Type directoryType; Type directoryRefType; if (parentDir is Wix.Directory || parentDir is Wix.DirectoryRef) { directoryType = typeof(Wix.Directory); directoryRefType = typeof(Wix.DirectoryRef); } else { throw new ArgumentException("GetSubDirElement parentDir"); } // Search for an existing directory element. foreach (Wix.ISchemaElement childElement in parentDir.Children) { if(VSProjectHarvester.AreTypesEquivalent(childElement.GetType(), directoryType)) { DirectoryAttributeAccessor childDir = new DirectoryAttributeAccessor(childElement); if (String.Equals(childDir.Name, firstSubDirName, StringComparison.OrdinalIgnoreCase)) { subDir = childDir; break; } } } if (subDir == null) { string parentId = null; DirectoryAttributeAccessor parentDirectory = null; DirectoryAttributeAccessor parentDirectoryRef = null; if (VSProjectHarvester.AreTypesEquivalent(parentDir.GetType(), directoryType)) { parentDirectory = new DirectoryAttributeAccessor((Wix.ISchemaElement)parentDir); } else if (VSProjectHarvester.AreTypesEquivalent(parentDir.GetType(), directoryRefType)) { parentDirectoryRef = new DirectoryAttributeAccessor((Wix.ISchemaElement)parentDir); } if (parentDirectory != null) { parentId = parentDirectory.Id; } else if (parentDirectoryRef != null) { if (this.setUniqueIdentifiers) { //Use the GUID of the project instead of the project name to help keep things stable. parentId = this.directoryRefSeed; } else { parentId = parentDirectoryRef.Id; } } Wix.ISchemaElement newDirectory = (Wix.ISchemaElement)directoryType.GetConstructor(new Type[] { }).Invoke(null); subDir = new DirectoryAttributeAccessor(newDirectory); if (this.setUniqueIdentifiers) { subDir.Id = this.Core.GenerateIdentifier(DirectoryPrefix, parentId, firstSubDirName); } else { subDir.Id = String.Format(DirectoryIdFormat, parentId, firstSubDirName); } subDir.Name = firstSubDirName; parentDir.AddChild(subDir.Element); } if (segments.Length == 1) { return subDir.ElementAsParent; } else { Uri nextRelativeUri = new Uri(Uri.UnescapeDataString(relativeUri.ToString()).Substring(firstSubDirName.Length + 1), UriKind.Relative); return this.GetSubDirElement(subDir.ElementAsParent, nextRelativeUri); } } private MSBuildProject GetMsbuildProject(string projectFile) { XmlDocument document = new XmlDocument(); try { document.Load(projectFile); } catch (Exception e) { throw new WixException(HarvesterErrors.CannotLoadProject(projectFile, e.Message)); } string version = null; if (this.UseToolsVersion) { foreach (XmlNode child in document.ChildNodes) { if (String.Equals(child.Name, "Project", StringComparison.Ordinal) && child.Attributes != null) { XmlNode toolsVersionAttribute = child.Attributes["ToolsVersion"]; if (toolsVersionAttribute != null) { version = toolsVersionAttribute.Value; this.Core.Messaging.Write(HarvesterVerboses.FoundToolsVersion(version)); break; } } } switch (version) { case "4.0": version = "4.0.0.0"; break; case "12.0": version = "12.0.0.0"; break; case "14.0": version = "14.0.0.0"; break; default: if (String.IsNullOrEmpty(this.MsbuildBinPath)) { throw new WixException(HarvesterErrors.MsbuildBinPathRequired(version ?? "(none)")); } version = null; break; } } var project = this.ConstructMsbuild40Project(version); return project; } private Assembly ResolveFromMsbuildBinPath(object sender, ResolveEventArgs args) { var assemblyName = new AssemblyName(args.Name); var assemblyPath = Path.Combine(this.MsbuildBinPath, $"{assemblyName.Name}.dll"); if (!File.Exists(assemblyPath)) { return null; } return Assembly.LoadFrom(assemblyPath); } private MSBuildProject ConstructMsbuild40Project(string loadVersion) { const string MSBuildEngineAssemblyName = "Microsoft.Build, Version={0}, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"; const string MSBuildFrameworkAssemblyName = "Microsoft.Build.Framework, Version={0}, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"; Assembly msbuildAssembly; Assembly msbuildFrameworkAssembly; if (loadVersion == null) { this.Core.Messaging.Write(HarvesterVerboses.LoadingProjectWithBinPath(this.MsbuildBinPath)); AppDomain.CurrentDomain.AssemblyResolve += this.ResolveFromMsbuildBinPath; try { msbuildAssembly = Assembly.Load("Microsoft.Build"); } catch (Exception e) { throw new WixException(HarvesterErrors.CannotLoadMSBuildAssembly(e.Message)); } try { msbuildFrameworkAssembly = Assembly.Load("Microsoft.Build.Framework"); } catch (Exception e) { throw new WixException(HarvesterErrors.CannotLoadMSBuildAssembly(e.Message)); } } else { this.Core.Messaging.Write(HarvesterVerboses.LoadingProjectWithVersion(loadVersion)); try { msbuildAssembly = Assembly.Load(String.Format(MSBuildEngineAssemblyName, loadVersion)); } catch (Exception e) { throw new WixException(HarvesterErrors.CannotLoadMSBuildAssembly(e.Message)); } try { msbuildFrameworkAssembly = Assembly.Load(String.Format(MSBuildFrameworkAssemblyName, loadVersion)); } catch (Exception e) { throw new WixException(HarvesterErrors.CannotLoadMSBuildAssembly(e.Message)); } } Type projectType; Type buildItemType; Type buildManagerType; Type buildParametersType; Type buildRequestDataFlagsType; Type buildRequestDataType; Type hostServicesType; Type projectCollectionType; Type projectInstanceType; Type writeHandlerType; Type colorSetterType; Type colorResetterType; Type loggerVerbosityType; Type consoleLoggerType; Type iLoggerType; try { buildItemType = msbuildAssembly.GetType("Microsoft.Build.Execution.ProjectItemInstance", true); projectType = msbuildAssembly.GetType("Microsoft.Build.Evaluation.Project", true); buildManagerType = msbuildAssembly.GetType("Microsoft.Build.Execution.BuildManager", true); buildParametersType = msbuildAssembly.GetType("Microsoft.Build.Execution.BuildParameters", true); buildRequestDataFlagsType = msbuildAssembly.GetType("Microsoft.Build.Execution.BuildRequestDataFlags", true); buildRequestDataType = msbuildAssembly.GetType("Microsoft.Build.Execution.BuildRequestData", true); hostServicesType = msbuildAssembly.GetType("Microsoft.Build.Execution.HostServices", true); projectCollectionType = msbuildAssembly.GetType("Microsoft.Build.Evaluation.ProjectCollection", true); projectInstanceType = msbuildAssembly.GetType("Microsoft.Build.Execution.ProjectInstance", true); writeHandlerType = msbuildAssembly.GetType("Microsoft.Build.Logging.WriteHandler", true); colorSetterType = msbuildAssembly.GetType("Microsoft.Build.Logging.ColorSetter", true); colorResetterType = msbuildAssembly.GetType("Microsoft.Build.Logging.ColorResetter", true); loggerVerbosityType = msbuildFrameworkAssembly.GetType("Microsoft.Build.Framework.LoggerVerbosity", true); consoleLoggerType = msbuildAssembly.GetType("Microsoft.Build.Logging.ConsoleLogger", true); iLoggerType = msbuildFrameworkAssembly.GetType("Microsoft.Build.Framework.ILogger", true); } catch (TargetInvocationException tie) { throw new WixException(HarvesterErrors.CannotLoadMSBuildEngine(tie.InnerException.Message)); } catch (Exception e) { throw new WixException(HarvesterErrors.CannotLoadMSBuildEngine(e.Message)); } MSBuild40Types types = new MSBuild40Types(); types.buildManagerType = buildManagerType; types.buildParametersType = buildParametersType; types.buildRequestDataFlagsType = buildRequestDataFlagsType; types.buildRequestDataType = buildRequestDataType; types.hostServicesType = hostServicesType; types.projectCollectionType = projectCollectionType; types.projectInstanceType = projectInstanceType; types.writeHandlerType = writeHandlerType; types.colorSetterType = colorSetterType; types.colorResetterType = colorResetterType; types.loggerVerbosityType = loggerVerbosityType; types.consoleLoggerType = consoleLoggerType; types.iLoggerType = iLoggerType; return new MSBuild40Project(null, projectType, buildItemType, loadVersion, types, this.Core, this.configuration, this.platform); } private static bool AreTypesEquivalent(Type a, Type b) { return (a == b) || (a.IsAssignableFrom(b) && b.IsAssignableFrom(a)); } private abstract class MSBuildProject { protected Type projectType; protected Type buildItemType; protected object project; private string loadVersion; public MSBuildProject(object project, Type projectType, Type buildItemType, string loadVersion) { this.project = project; this.projectType = projectType; this.buildItemType = buildItemType; this.loadVersion = loadVersion; } public string LoadVersion { get { return this.loadVersion; } } public abstract bool Build(string projectFileName, string[] targetNames, IDictionary targetOutputs); public abstract MSBuildProjectItemType GetBuildItem(object buildItem); public abstract IEnumerable GetEvaluatedItemsByName(string itemName); public abstract string GetEvaluatedProperty(string propertyName); public abstract void Load(string projectFileName); } private abstract class MSBuildProjectItemType { public MSBuildProjectItemType(object buildItem) { this.buildItem = buildItem; } public abstract override string ToString(); public abstract string GetMetadata(string name); protected object buildItem; } private struct MSBuild40Types { public Type buildManagerType; public Type buildParametersType; public Type buildRequestDataFlagsType; public Type buildRequestDataType; public Type hostServicesType; public Type projectCollectionType; public Type projectInstanceType; public Type writeHandlerType; public Type colorSetterType; public Type colorResetterType; public Type loggerVerbosityType; public Type consoleLoggerType; public Type iLoggerType; } private class MSBuild40Project : MSBuildProject { private MSBuild40Types types; private object projectCollection; private object currentProjectInstance; private object buildManager; private object buildParameters; private IHarvesterCore harvesterCore; public MSBuild40Project(object project, Type projectType, Type buildItemType, string loadVersion, MSBuild40Types types, IHarvesterCore harvesterCore, string configuration, string platform) : base(project, projectType, buildItemType, loadVersion) { this.types = types; this.harvesterCore = harvesterCore; this.buildParameters = this.types.buildParametersType.GetConstructor(new Type[] { }).Invoke(null); try { var loggers = this.CreateLoggers(); // this.buildParameters.Loggers = loggers; this.types.buildParametersType.GetProperty("Loggers").SetValue(this.buildParameters, loggers, null); } catch (TargetInvocationException tie) { if (this.harvesterCore != null) { this.harvesterCore.Messaging.Write(HarvesterWarnings.NoLogger(tie.InnerException.Message)); } } catch (Exception e) { if (this.harvesterCore != null) { this.harvesterCore.Messaging.Write(HarvesterWarnings.NoLogger(e.Message)); } } this.buildManager = this.types.buildManagerType.GetConstructor(new Type[] { }).Invoke(null); if (configuration != null || platform != null) { Dictionary globalVariables = new Dictionary(); if (configuration != null) { globalVariables.Add("Configuration", configuration); } if (platform != null) { globalVariables.Add("Platform", platform); } this.projectCollection = this.types.projectCollectionType.GetConstructor(new Type[] { typeof(IDictionary) }).Invoke(new object[] { globalVariables }); } else { this.projectCollection = this.types.projectCollectionType.GetConstructor(new Type[] {}).Invoke(null); } } private object CreateLoggers() { var logger = new HarvestLogger(this.harvesterCore.Messaging); var loggerVerbosity = Enum.Parse(this.types.loggerVerbosityType, "Minimal"); var writeHandler = Delegate.CreateDelegate(this.types.writeHandlerType, logger, nameof(logger.LogMessage)); var colorSetter = Delegate.CreateDelegate(this.types.colorSetterType, logger, nameof(logger.SetColor)); var colorResetter = Delegate.CreateDelegate(this.types.colorResetterType, logger, nameof(logger.ResetColor)); var consoleLoggerCtor = this.types.consoleLoggerType.GetConstructor(new Type[] { this.types.loggerVerbosityType, this.types.writeHandlerType, this.types.colorSetterType, this.types.colorResetterType, }); var consoleLogger = consoleLoggerCtor.Invoke(new object[] { loggerVerbosity, writeHandler, colorSetter, colorResetter }); var loggers = Array.CreateInstance(this.types.iLoggerType, 1); loggers.SetValue(consoleLogger, 0); return loggers; } public override bool Build(string projectFileName, string[] targetNames, IDictionary targetOutputs) { try { // this.buildManager.BeginBuild(this.buildParameters); this.types.buildManagerType.GetMethod("BeginBuild", new Type[] { this.types.buildParametersType }).Invoke(this.buildManager, new object[] { this.buildParameters }); // buildRequestData = new BuildRequestData(this.currentProjectInstance, targetNames, null, BuildRequestData.BuildRequestDataFlags.ReplaceExistingProjectInstance); ConstructorInfo buildRequestDataCtor = this.types.buildRequestDataType.GetConstructor( new Type[] { this.types.projectInstanceType, typeof(string[]), this.types.hostServicesType, this.types.buildRequestDataFlagsType }); object buildRequestDataFlags = this.types.buildRequestDataFlagsType.GetField("ReplaceExistingProjectInstance").GetRawConstantValue(); object buildRequestData = buildRequestDataCtor.Invoke(new object[] { this.currentProjectInstance, targetNames, null, buildRequestDataFlags }); // BuildSubmission submission = this.buildManager.PendBuildRequest(buildRequestData); object submission = this.types.buildManagerType.GetMethod("PendBuildRequest", new Type[] { this.types.buildRequestDataType }) .Invoke(this.buildManager, new object[] { buildRequestData }); // BuildResult buildResult = submission.Execute(); object buildResult = submission.GetType().GetMethod("Execute", new Type[] { }).Invoke(submission, null); // bool buildSucceeded = buildResult.OverallResult == BuildResult.Success; object overallResult = buildResult.GetType().GetProperty("OverallResult").GetValue(buildResult, null); bool buildSucceeded = String.Equals(overallResult.ToString(), "Success", StringComparison.Ordinal); // this.buildManager.EndBuild(); this.types.buildManagerType.GetMethod("EndBuild", new Type[] { }).Invoke(this.buildManager, null); // fill in empty lists for each target so that heat will look at the item group later foreach (string target in targetNames) { targetOutputs.Add(target, new List()); } return buildSucceeded; } catch (TargetInvocationException tie) { throw new WixException(HarvesterErrors.CannotBuildProject(projectFileName, tie.InnerException.Message)); } catch (Exception e) { throw new WixException(HarvesterErrors.CannotBuildProject(projectFileName, e.Message)); } } public override MSBuildProjectItemType GetBuildItem(object buildItem) { return new MSBuild40ProjectItemType(buildItem); } public override IEnumerable GetEvaluatedItemsByName(string itemName) { MethodInfo getEvaluatedItem = this.types.projectInstanceType.GetMethod("GetItems", new Type[] { typeof(string) }); return (IEnumerable)getEvaluatedItem.Invoke(this.currentProjectInstance, new object[] { itemName }); } public override string GetEvaluatedProperty(string propertyName) { MethodInfo getProperty = this.types.projectInstanceType.GetMethod("GetPropertyValue", new Type[] { typeof(string) }); return (string)getProperty.Invoke(this.currentProjectInstance, new object[] { propertyName }); } public override void Load(string projectFileName) { try { //this.project = this.projectCollection.LoadProject(projectFileName); this.project = this.types.projectCollectionType.GetMethod("LoadProject", new Type[] { typeof(string) }).Invoke(this.projectCollection, new object[] { projectFileName }); // this.currentProjectInstance = this.project.CreateProjectInstance(); MethodInfo createProjectInstanceMethod = this.projectType.GetMethod("CreateProjectInstance", new Type[] { }); this.currentProjectInstance = createProjectInstanceMethod.Invoke(this.project, null); } catch (TargetInvocationException tie) { throw new WixException(HarvesterErrors.CannotLoadProject(projectFileName, tie.InnerException.Message)); } catch (Exception e) { throw new WixException(HarvesterErrors.CannotLoadProject(projectFileName, e.Message)); } } } private class MSBuild40ProjectItemType : MSBuildProjectItemType { public MSBuild40ProjectItemType(object buildItem) : base(buildItem) { } public override string ToString() { PropertyInfo includeProperty = this.buildItem.GetType().GetProperty("EvaluatedInclude"); return (string)includeProperty.GetValue(this.buildItem, null); } public override string GetMetadata(string name) { MethodInfo getMetadataMethod = this.buildItem.GetType().GetMethod("GetMetadataValue"); if (null != getMetadataMethod) { return (string)getMetadataMethod.Invoke(this.buildItem, new object[] { name }); } return string.Empty; } } /// /// Used internally in the VSProjectHarvester class to encapsulate /// the settings for a particular MSBuild "project output group". /// private struct ProjectOutputGroup { public readonly string Name; public readonly string BuildOutputGroup; public readonly string FileSource; /// /// Creates a new project output group. /// /// Friendly name used by heat. /// MSBuild's name of the project output group. /// VS directory token containing the files of the POG. public ProjectOutputGroup(string name, string buildOutputGroup, string fileSource) { this.Name = name; this.BuildOutputGroup = buildOutputGroup; this.FileSource = fileSource; } } /// /// Internal class for getting and setting common attrbiutes on /// directory elements. /// internal class DirectoryAttributeAccessor { public Wix.ISchemaElement directoryElement; public DirectoryAttributeAccessor(Wix.ISchemaElement directoryElement) { this.directoryElement = directoryElement; } /// /// Gets the element as a ISchemaElement. /// public Wix.ISchemaElement Element { get { return this.directoryElement; } } /// /// Gets the element as a IParentElement. /// public Wix.IParentElement ElementAsParent { get { return (Wix.IParentElement)this.directoryElement; } } /// /// Gets or sets the Id attrbiute. /// public string Id { get { if (this.directoryElement is Wix.Directory wixDirectory) { return wixDirectory.Id; } else if (this.directoryElement is Wix.DirectoryRef wixDirectoryRef) { return wixDirectoryRef.Id; } else { throw new WixException(HarvesterErrors.DirectoryAttributeAccessorBadType("Id")); } } set { if (this.directoryElement is Wix.Directory wixDirectory) { wixDirectory.Id = value; } else if (this.directoryElement is Wix.DirectoryRef wixDirectoryRef) { wixDirectoryRef.Id = value; } else { throw new WixException(HarvesterErrors.DirectoryAttributeAccessorBadType("Id")); } } } /// /// Gets or sets the Name attribute. /// public string Name { get { if (this.directoryElement is Wix.Directory wixDirectory) { return wixDirectory.Name; } else { throw new WixException(HarvesterErrors.DirectoryAttributeAccessorBadType("Name")); } } set { if (this.directoryElement is Wix.Directory wixDirectory) { wixDirectory.Name = value; } else { throw new WixException(HarvesterErrors.DirectoryAttributeAccessorBadType("Name")); } } } } internal class HarvestLogger { public HarvestLogger(IMessaging messaging) { this.Color = ConsoleColor.Black; this.Messaging = messaging; } private ConsoleColor Color { get; set; } private IMessaging Messaging { get; } public void LogMessage(string message) { if (this.Color == ConsoleColor.Red) { this.Messaging.Write(HarvesterErrors.BuildErrorDuringHarvesting(message)); } } public void SetColor(ConsoleColor color) { this.Color = color; } public void ResetColor() { this.Color = ConsoleColor.Black; } } } }