Skip to content

Instantly share code, notes, and snippets.

@afranchuk
Last active October 27, 2023 14:00
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save afranchuk/bf4c77aca5e653ade3e2fec2289eedc6 to your computer and use it in GitHub Desktop.
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.
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