2

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.

thomas_g
  • 21
  • 5
  • 1
    Is the code from `Main` running? [Could this solution be useful?](https://stackoverflow.com/questions/52797/how-do-i-get-the-path-of-the-assembly-the-code-is-in) – Ron Beyer Feb 13 '18 at 00:26
  • @ronbeyer I need to know the location before the user executes `Main` – thomas_g Feb 13 '18 at 01:15
  • @thomas_g you don't need the location before the user executes `Main`. You need interfaces in a dll that is referenced in Main and Plugin1 and Plugin2. That way you have the functionality to compile Main based on functionality in Plugin1/2. You can load Plugin1 etc at runtime after Main is loaded. (At least this would be how this situation would be typically done as best practice). Alternately you could use refelection once a plugin is loaded (but this would be slow and fairly dangerous being loosey goosey). – Dessus Feb 13 '18 at 01:56
  • @Dessus commented on your answer below – thomas_g Feb 13 '18 at 03:28
  • The question is too broad. There are many types of projects in VS (not only C# or VB.NET). Some don't have the concept of an output path. You should specialize your code for some specific project types. When you do that, you can presume more about these projets (for example: C# and VB.NET use MSBuild) and rephrase or ask another question – Simon Mourier Feb 13 '18 at 07:18
  • @SimonMourier edited to specify project type – thomas_g Feb 13 '18 at 08:00

2 Answers2

0

As I see it you have the following options:

  1. You could likely put the dlls inside a vsix file see here which is a zip file. You can use reflection from your entry point (ie main method) and then you can find your main methods executable location (ie exe, dll etc). If it shows as in vsix path then you know your vsix location and can load the other dlls relative to that.

  2. You can work around your issue by having the dlls download as required see here with a framework like MEF or MAF see here. That way you don't need to know the location. You could also choose to store the downloaded dlls standalone or within a section of your dll as a resource etc. (your choice)

For both options you would end up creating interfaces in new dlls that would be refernced by both the plugins and your main project. This will enable you to have certainty between your different projects. There is nothing that an interface can't represent practically as you can even put function call backs in there.

Dessus
  • 2,147
  • 1
  • 14
  • 24
  • `The main problem I have is that I don't have a reliable way of retrieving the "final output path" of the plugins`. That could be solved by pushing the dlls to GAC, it could be solved by adding registry entries detailing where they are as part of the build process. It could controlled via a config in the main project which also controls where the plugins are deployed to. It could be controlled via VSIX manifest etc. – Dessus Feb 13 '18 at 01:43
  • `I can't just change the project files, since this extension needs to work with as many VS solutions as possible` That isn't true, as you can have many project targets or you can use msbuild to multitarget by switching out select values in the csproj etc. and have those as separate downloads etc. – Dessus Feb 13 '18 at 01:52
  • 1
    I'm not sure we're talking about the same things. When I say "projects" and "solutions", I'm not talking about my extension. I'm talking about the projects and solutions that the end user is working on while they're using my extension. Maybe I was unclear, or maybe I'm misunderstanding your answer? – thomas_g Feb 13 '18 at 03:25
0

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.

thomas_g
  • 21
  • 5