I'm writing a Visual Studio extension, and I need to know the final location of the end user's project's executable for each of their projects. Let's assume I'm specifically targeting C# desktop applications, so they should be using MSBuild. This is normally fairly simple, but some end user projects can be quite complex. The simple answer is to query DTE
for each project and get their OutputPath
. Sometimes, it's not so simple. Here's an example where this doesn't work:
Some solution contains three projects: Main
, Plugin1
, and Plugin2
.
The Main
project uses the standard output path.
The Plugin1
and Plugin2
projects get copied to a plugins
folder after they're built, through a Copy AfterBuild
directive in their respective project files.
The user runs the Main
project and tells it, at run-time, which plugin they need.
The Main
project uses that information to dynamically load the selected plugin.
Note that this means that the selected DLLs are not shown as references in the Main
executable. If they were, I could figure out a way to retrieve that information, but they're dynamically calculated. I need to know this information before execution.
The main problem I have is that I don't have a reliable way of retrieving the "final output path" of the plugins (the result of the AfterBuild
directive in the project files), and that is what I really need to know. Unfortunately, I can't just change the project files, since this extension needs to work with as many VS solutions as possible.
Update: I've experimented with the MSBuild API, using a combination of a custom logger and the FileWrites
variable, but I can't find a way to extract this information. Unfortunately, FileWrites
doesn't hold the results of Copy
operations. Unless someone presents a better solution, I'll just crawl the solution tree for all files that "match" the target (size, timestamp, contents, etc.). It's admittedly a hack, but I don't see a better way.
Might as well close out this question. Never found an elegant answer, so I'll sketch out my brute force approach. This first method just returns all executables (DLLs and EXEs) in the given path, recursively:
IEnumerable<FileInfo> FindBinaries(string path)
{
var dirInfo = new DirectoryInfo(path);
var exeFiles = dirInfo.EnumerateFiles("*.exe", SearchOption.AllDirectories);
var dllFiles = dirInfo.EnumerateFiles("*.dll", SearchOption.AllDirectories);
return exeFiles.Concat(dllFiles);
}
Here is a method that calculates a checksum:
string GetChecksum(string filename)
{
string checksum = null;
using (var md5 = System.Security.Cryptography.MD5.Create())
{
using (var stream = File.OpenRead(filename))
{
checksum = BitConverter.ToString(md5.ComputeHash(stream));
}
}
return checksum;
}
Here's the method that performs the work:
IEnumerable<string> FindMatchingBinaries(string path, string filename)
{
var checksum = GetChecksum(filename);
return FindBinaries(path).Select(p => p.FullName).Where(name => GetChecksum(name) == checksum);
}
You'd then call it with the proper parameters, like this:
var matchingFiles = FindMatchingBinaries(solutionRoot, projectBinaryName);
Of course, you need to determine what the solution root and the project binary are, elsewhere. They're a little out of the scope of the problem, and there are plenty of other answers for these particular problems.