Last active
October 27, 2023 14:00
-
-
Save afranchuk/bf4c77aca5e653ade3e2fec2289eedc6 to your computer and use it in GitHub Desktop.
An MSBuild Task to automatically load project dependencies for WIX to bundle into an installer.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Build.Framework; | |
using Microsoft.Build.Utilities; | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Text; | |
using System.Xml; | |
namespace BuildTasks | |
{ | |
public class WIXProjectDependencies : Task | |
{ | |
[Required] | |
public string ProjectFile { get; set; } | |
[Required] | |
public string OutputFile { get; set; } | |
public string Configuration { get; set; } | |
public bool GenerateExternalIds { get; set; } = true; | |
public bool GenerateInternalIds { get; set; } | |
public bool GenerateGuids { get; set; } = true; | |
public override bool Execute() | |
{ | |
string wixproj = ProjectFile; | |
string outputFile = OutputFile; | |
string configuration = "Debug"; | |
if (Configuration != null) | |
{ | |
configuration = Configuration; | |
} | |
XmlDocument doc = new XmlDocument(); | |
try | |
{ | |
doc.Load(wixproj); | |
} | |
catch (Exception) | |
{ | |
Log.LogError("Invalid wixproj file."); | |
return false; | |
} | |
HashSet<string> topLevelProjects = new HashSet<string>(); | |
Queue<string> projects = new Queue<string>(); | |
HashSet<string> enqueued_projects = new HashSet<string>(); | |
HashSet<ProjFile> files = new HashSet<ProjFile>(); | |
Dictionary<string, Project> deps = new Dictionary<string, Project>(); | |
//Get project references in wix project file | |
var prefs = doc.GetElementsByTagName("ProjectReference"); | |
foreach (XmlNode pref in prefs) | |
{ | |
string projfile = null; | |
try | |
{ | |
projfile = pref.Attributes["Include"].Value; | |
} | |
catch (Exception) | |
{ | |
continue; | |
} | |
projects.Enqueue(projfile); | |
enqueued_projects.Add(Path.GetFullPath(projfile)); | |
topLevelProjects.Add(ProjectFileToName(projfile)); | |
} | |
//Iterate through projects and store dependencies of each | |
while (projects.Count > 0) | |
{ | |
string projfile = projects.Dequeue(); | |
XmlDocument projdoc = new XmlDocument(); | |
projdoc.Load(projfile); | |
string projdir = Path.GetDirectoryName(projfile); | |
string projname = ProjectFileToName(projfile); | |
string projtype = Path.GetExtension(projfile).TrimStart('.'); | |
Dictionary<string, string> project_properties = new Dictionary<string, string>(); | |
//Create project properties based on current configuration | |
foreach (XmlNode node in projdoc.GetElementsByTagName("PropertyGroup")) | |
{ | |
string cond = node.Attributes["Condition"]?.Value; | |
if (cond == null || cond.ToLower().Contains(configuration.ToLower())) | |
{ | |
foreach (XmlNode child in node) | |
{ | |
if (project_properties.ContainsKey(child.Name)) | |
{ | |
project_properties[child.Name] = child.InnerText; | |
} | |
else | |
{ | |
project_properties.Add(child.Name, child.InnerText); | |
} | |
} | |
} | |
} | |
string projExtension = null; | |
string assemblyName = null; | |
string outputPath = null; | |
string outputType = null; | |
if (projtype == "csproj") | |
{ | |
if (!project_properties.ContainsKey("OutputType") || !project_properties.ContainsKey("AssemblyName") || | |
!project_properties.ContainsKey("OutputPath")) | |
{ | |
Log.LogError("Invalid project file " + projfile); | |
return false; | |
} | |
outputType = project_properties["OutputType"]; | |
assemblyName = project_properties["AssemblyName"]; | |
outputPath = project_properties["OutputPath"]; | |
} | |
else if (projtype == "vcxproj") | |
{ | |
if (!project_properties.ContainsKey("RootNamespace") || !project_properties.ContainsKey("OutDir") || | |
!project_properties.ContainsKey("ConfigurationType")) | |
{ | |
Log.LogError("Invalid project file " + projfile); | |
return false; | |
} | |
outputType = project_properties["ConfigurationType"]; | |
assemblyName = project_properties["RootNamespace"]; | |
outputPath = project_properties["OutDir"]; | |
} | |
else | |
{ | |
Log.LogError("Unrecognized project type: '{0}' ({1})", projtype, projfile); | |
return false; | |
} | |
if (exeOutputTypes.Contains(outputType.ToLower())) | |
{ | |
projExtension = ".exe"; | |
} | |
else if (dllOutputTypes.Contains(outputType.ToLower())) | |
{ | |
projExtension = ".dll"; | |
} | |
else | |
{ | |
Log.LogError("Unrecognized project output type: '{0}' ({1})", outputType, projfile); | |
return false; | |
} | |
if (assemblyName == null || outputPath == null || projExtension == null) | |
{ | |
Log.LogError("Implementation error: project type {0} does not extract all required information.", projtype); | |
return false; | |
} | |
ProjectOutputFile resultFile = new ProjectOutputFile(Path.GetFullPath(Path.Combine(projdir, outputPath, assemblyName + projExtension))); | |
files.Add(resultFile); | |
deps.Add(projname, new Project(resultFile)); | |
//Save dll references to external dlls (from NuGet) | |
foreach (XmlNode node in projdoc.GetElementsByTagName("Reference")) | |
{ | |
XmlElement hint = node["HintPath"]; | |
if (hint != null) | |
{ | |
ProjFile file = new ExternalFile(Path.GetFullPath(Path.Combine(projdir, hint.InnerText))); | |
files.Add(file); | |
deps[projname].Dependencies.Add(new FileDependency(file)); | |
} | |
} | |
//Save project references | |
foreach (XmlNode node in projdoc.GetElementsByTagName("ProjectReference")) | |
{ | |
string include = node.Attributes["Include"]?.Value; | |
if (include != null) | |
{ | |
string newprojfile = Path.GetFullPath(Path.Combine(projdir, include)); | |
if (!enqueued_projects.Contains(newprojfile)) | |
{ | |
projects.Enqueue(newprojfile); | |
enqueued_projects.Add(newprojfile); | |
} | |
deps[projname].Dependencies.Add(new ProjectDependency(ProjectFileToName(newprojfile))); | |
} | |
} | |
} | |
//Create output XmlDocument | |
XmlDocument output = new XmlDocument(); | |
output.AppendChild(output.CreateXmlDeclaration("1.0", "utf-8", null)); | |
XmlElement root = output.CreateElement("Wix", "http://schemas.microsoft.com/wix/2006/wi"); | |
output.AppendChild(root); | |
XmlElement frag = output.CreateElement("Fragment", output.DocumentElement.NamespaceURI); | |
root.AppendChild(frag); | |
XmlElement dirref = output.CreateElement("DirectoryRef", output.DocumentElement.NamespaceURI); | |
dirref.SetAttribute("Id", "INSTALLFOLDER"); | |
frag.AppendChild(dirref); | |
//Reference files and save the component ids of each | |
Dictionary<ProjFile, string> componentId = new Dictionary<ProjFile, string>(); | |
foreach (var file in files) | |
{ | |
XmlElement cmp = output.CreateElement("Component", output.DocumentElement.NamespaceURI); | |
string id = MakeId(file, "cmp"); | |
cmp.SetAttribute("Id", id); | |
string guid = "*"; | |
if (GenerateGuids) | |
{ | |
guid = Guid.NewGuid().ToString(); | |
} | |
cmp.SetAttribute("Guid", guid); | |
componentId.Add(file, id); | |
XmlElement f = output.CreateElement("File", output.DocumentElement.NamespaceURI); | |
f.SetAttribute("Id", MakeId(file, "fil")); | |
f.SetAttribute("Source", file.FileName); | |
cmp.AppendChild(f); | |
dirref.AppendChild(cmp); | |
} | |
ProjectWalker walker = new ProjectWalker(deps, componentId); | |
foreach (var project in topLevelProjects) | |
{ | |
XmlElement proj_frag = output.CreateElement("Fragment", output.DocumentElement.NamespaceURI); | |
root.AppendChild(proj_frag); | |
XmlElement cmpGroup = output.CreateElement("ComponentGroup", output.DocumentElement.NamespaceURI); | |
proj_frag.AppendChild(cmpGroup); | |
cmpGroup.SetAttribute("Id", project); | |
foreach (var cmpId in walker.GetComponentIds(project)) | |
{ | |
XmlElement cmpref = output.CreateElement("ComponentRef", output.DocumentElement.NamespaceURI); | |
cmpref.SetAttribute("Id", cmpId); | |
cmpGroup.AppendChild(cmpref); | |
} | |
} | |
output.Save(outputFile); | |
return true; | |
} | |
private static readonly string[] exeOutputTypes = new string[] | |
{ | |
"winexe", "exe" | |
}; | |
private static readonly string[] dllOutputTypes = new string[] | |
{ | |
"library", "dynamiclibrary" | |
}; | |
static string ProjectFileToName(string file) | |
{ | |
return Path.GetFileNameWithoutExtension(file); | |
} | |
private string MakeId(ProjFile file, string prefix) | |
{ | |
if (GenerateExternalIds && file is ExternalFile || GenerateInternalIds && file is ProjectOutputFile) | |
{ | |
return string.Format("{0}{1:N}", prefix, Guid.NewGuid()); | |
} | |
else | |
{ | |
return Path.GetFileName(file.FileName); | |
} | |
} | |
} | |
class ProjectWalker | |
{ | |
public ProjectWalker(Dictionary<string, Project> projects, Dictionary<ProjFile, string> componentIds) | |
{ | |
this.projects = projects; | |
this.componentIds = componentIds; | |
} | |
private string[] _GetComponentIds(string projectName) | |
{ | |
if (!cache.ContainsKey(projectName)) | |
{ | |
var project = projects[projectName]; | |
HashSet<string> els = new HashSet<string>(); | |
els.Add(componentIds[project.Output]); | |
foreach (var dep in project.Dependencies) | |
{ | |
if (dep is FileDependency) | |
{ | |
els.Add(componentIds[(dep as FileDependency).File]); | |
} | |
else if (dep is ProjectDependency) | |
{ | |
els.UnionWith(_GetComponentIds((dep as ProjectDependency).ProjectName)); | |
} | |
} | |
cache.Add(projectName, els.ToArray()); | |
} | |
return cache[projectName]; | |
} | |
public IEnumerable<string> GetComponentIds(string projectName) | |
{ | |
return _GetComponentIds(projectName); | |
} | |
private Dictionary<string, string[]> cache = new Dictionary<string, string[]>(); | |
private Dictionary<string, Project> projects; | |
private Dictionary<ProjFile, string> componentIds; | |
} | |
abstract class ProjFile | |
{ | |
public string FileName { get; } | |
protected ProjFile(string filename) | |
{ | |
FileName = filename; | |
} | |
public override int GetHashCode() | |
{ | |
return FileName.GetHashCode(); | |
} | |
public override bool Equals(object obj) | |
{ | |
if (obj is ProjFile) | |
return FileName.Equals((obj as ProjFile).FileName); | |
return false; | |
} | |
public override string ToString() | |
{ | |
return FileName.ToString(); | |
} | |
} | |
class ProjectOutputFile : ProjFile | |
{ | |
public ProjectOutputFile(string file) : base(file) { } | |
} | |
class ExternalFile : ProjFile | |
{ | |
public ExternalFile(string file) : base(file) { } | |
} | |
class Project | |
{ | |
public ProjectOutputFile Output { get; } | |
public HashSet<Dependency> Dependencies { get; } | |
public Project(ProjectOutputFile output) | |
{ | |
Output = output; | |
Dependencies = new HashSet<Dependency>(); | |
} | |
} | |
abstract class Dependency | |
{ | |
} | |
class ProjectDependency : Dependency | |
{ | |
public string ProjectName { get; } | |
public ProjectDependency(string name) | |
{ | |
ProjectName = name; | |
} | |
} | |
class FileDependency : Dependency | |
{ | |
public ProjFile File { get; } | |
public FileDependency(ProjFile file) | |
{ | |
File = file; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment