Setting the OutputPath property of a project via Visual Studio Automation
Asked Answered
C

2

14

I'm writing a VSIX package to allow the user to bulk-edit the OutputPath property of all the active configurations of projects in the currently loaded solution (see the incredibly annoying step #4 here).

I ran into a very specific problem: when setting the property to a value containing macros (e.g. "$(SolutionDir)\bin\Debug" the value written into the .csproj is escaped as follows:

<OutputPath>%24%28SolutionDir%29\bin\Debug\</OutputPath>

Which, rather than letting MSBuild expand the macro, creates an actual physical folder named $(SolutionDir). I'd like to somehow bypass this escaping.

The MSDN documentation is unsurprisingly lacking in that area.

My initial code is as follows:

private void MenuItemCallback(object sender, EventArgs e)
{
    SolutionWideOutputDialogWindow dialog = new SolutionWideOutputDialogWindow();
    dialog.ShowModal();
    if (!dialog.DialogResult.HasValue || !dialog.DialogResult.Value)
    {
        return;
    }

    string requestedOutputPath = dialog.outputPathTextBox.Text;
    Solution2 solution = _dte2.Solution as Solution2;
    if (solution == null)
    {
        return;
    }

    Projects projects = solution.Projects;
    foreach (Project project in projects)
    {
        Property outputPath = project.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath");
        outputPath.Value = requestedOutputPath;
        project.Save();
    }
}

Greatly appreciate anyone's help.

Cyclic answered 29/11, 2013 at 14:42 Comment(4)
The exact same thing happens when you do it with the IDE. Nor does the IDE give any hint whatsoever that macros are supported. So pretty safe to assume that you just cannot make this work. Use a relative path instead, like ..\bin\debug.Porphyroid
Well, MSBuild does support it, but that leaves me with editing the underlying project files directlyCyclic
The msbuild/VS way of "do x for all projects" is using a common file that gets imported in all projects. You can do this manually (or scripted) by adding that file in each project, or you can use a global one (for example in $(VCTargetsPath)\Platforms\Win32\ImportBefore). Set OutputPath in the global one and you're done. I really recommend using a common file for defining pretty much everything (compiler/linker/tools options and all paths) anyway, it's a matter of DRY after all.Hamlani
That's actually what I had already done, but I encountered a problem when I wanted to make the output path global on a per-solution-configuration basis (i.e. $(SolutionDir)bin\$(SolutionConfigurationName)\$(SolutionPlatformName)). The macros exported by MSBuild are on a per-project basis and so would not work if you had project platforms which are not completely uniformly named. I eventually found a Visual Studio plugin to export these for me from within the IDE. I had to tweak it a bit to make it export them earlier and not break VS functionality, but I'm satisfied now.Cyclic
C
12

Here's what I ended up doing:

The problem I was trying to solve is not repeating myself (D.R.Y.) and specifying a solution-wide output directory (in a solution with a lot of projects) - that is, when compiling a solution, all projects would have their output directory set to something like $(SolutionDir)bin\Debug or $(SolutionDir)bin\Release. It is worth mentioning that some projects are included across repositories and in more than one solution.

At first, I created an MSBuild file (a <Project> XML - called it MySolution.sln.targets). In it, I defined a <PropertyGroup> that overrode the <OutputPath> property to:

$(SolutionDir)bin\$(Platform)\$(Configuration)

Then I added the following import to all the relevant projects, before the build targets import:

<Import Project="$(SolutionPath).targets" />

That way, each solution has an accompanying .targets file defining such things that I want to be solution-wide.

This worked well, but then I encountered the following problem: The abovementioned $(Platform) and $(Configuration) macros refer to the project's properties, not the solution-wide ones. What would happen if my solution's Debug/Any CPU configuration still built some very specific project in its Release configuration? As far as I'm aware, after thoroughly examining the documentation, no such macros are exported that have a solution-wide granularity.

I found ceztko's Visual Studio extension which made Visual Studio export exactly the macros that I was looking for - but after some experimentation and fiddling, I discovered that this extension set them too late - only upon building the solution. This caused issues with Visual Studio's Incremental Build feature - It kept thinking that the projects were out of date because it was looking in the wrong place - it wasn't aware of the variables, but MSBuild.exe was.

I started fiddling with the IVsUpdateSolutionEvents interface, tracing when each method is called - and then discovered that IVsUpdateSolutionEvents.OnActiveProjectCfgChange is called twice when opening a 1-project solution in a fresh Visual Studio, or when changing the solution's configuration from Debug to Release. Further fiddling revealed that if I set the project to compile in Release in both solution configurations, this method now got called once instead of twice when changing solution configurations.

I forked the extension's repository and amended the issue by moving the macro-setting logic to the above-mentioned method. You can find it here.

Disclaimer: this may not interact so well with batch-build operations from the IDE, and requires you to export these properties yourself when building from MSBuild.exe's command-line.

Good luck in your travels.

Cyclic answered 30/11, 2013 at 20:28 Comment(1)
Awesome work (the original project now seems to work)Remember
J
15

Visual Studio will unfortunately escape special characters when editing from project properties.

To fix this, edit your .csproj file directly in a text editor.

For example, change:

<OutputPath>%24%28SolutionDir%29\bin\Debug\</OutputPath>

to:

<OutputPath>$(SolutionDir)\bin\Debug\</OutputPath>
Jetpropelled answered 3/7, 2014 at 18:21 Comment(1)
BE ADVISED: If you use a text editor set the set the <OutputPath> to $(SolutionDir)\bin\Debug it will appear as ..\..\..\bin\Debug in Visual Studio 2013. The good news is, the macro will remain intact. As expected, if you make any changes to the Output Path: in Visual Studio... then the macro will be deleted. FYI - Microsoft uses this website to help prioritize feature requests: UserVoice.comHampton
C
12

Here's what I ended up doing:

The problem I was trying to solve is not repeating myself (D.R.Y.) and specifying a solution-wide output directory (in a solution with a lot of projects) - that is, when compiling a solution, all projects would have their output directory set to something like $(SolutionDir)bin\Debug or $(SolutionDir)bin\Release. It is worth mentioning that some projects are included across repositories and in more than one solution.

At first, I created an MSBuild file (a <Project> XML - called it MySolution.sln.targets). In it, I defined a <PropertyGroup> that overrode the <OutputPath> property to:

$(SolutionDir)bin\$(Platform)\$(Configuration)

Then I added the following import to all the relevant projects, before the build targets import:

<Import Project="$(SolutionPath).targets" />

That way, each solution has an accompanying .targets file defining such things that I want to be solution-wide.

This worked well, but then I encountered the following problem: The abovementioned $(Platform) and $(Configuration) macros refer to the project's properties, not the solution-wide ones. What would happen if my solution's Debug/Any CPU configuration still built some very specific project in its Release configuration? As far as I'm aware, after thoroughly examining the documentation, no such macros are exported that have a solution-wide granularity.

I found ceztko's Visual Studio extension which made Visual Studio export exactly the macros that I was looking for - but after some experimentation and fiddling, I discovered that this extension set them too late - only upon building the solution. This caused issues with Visual Studio's Incremental Build feature - It kept thinking that the projects were out of date because it was looking in the wrong place - it wasn't aware of the variables, but MSBuild.exe was.

I started fiddling with the IVsUpdateSolutionEvents interface, tracing when each method is called - and then discovered that IVsUpdateSolutionEvents.OnActiveProjectCfgChange is called twice when opening a 1-project solution in a fresh Visual Studio, or when changing the solution's configuration from Debug to Release. Further fiddling revealed that if I set the project to compile in Release in both solution configurations, this method now got called once instead of twice when changing solution configurations.

I forked the extension's repository and amended the issue by moving the macro-setting logic to the above-mentioned method. You can find it here.

Disclaimer: this may not interact so well with batch-build operations from the IDE, and requires you to export these properties yourself when building from MSBuild.exe's command-line.

Good luck in your travels.

Cyclic answered 30/11, 2013 at 20:28 Comment(1)
Awesome work (the original project now seems to work)Remember

© 2022 - 2024 — McMap. All rights reserved.