Relative paths with MSBuild project vs solution - visual-studio-2010

I have a number of projects which are joined into a solution. Every project has it's own directory structure, and csproj files are located on diferrent level of folder structure.
Every csproj has OutputPath property specified. OutputPath - is a relative path and it varies from project to project in such a way so all projects have the same output dir.
It is work OK if I build a separate project. But everything changes if I try to build solution file. In this case every project output folder differs (depends on a number of '..\' in that project's OutputPath).
I do know, that before some moment all was working fine. Nobody changed build.cmd neither any sln or csproj files. But now I have situation described above.
So my question is - what affects how relative path is evaluated? I mean how can I force relative OutputPath to be evaluated starting from folder where csproj file of that particular project is located. Not from folder where .sln file is.
Let's assume I have following directory structure:
dir1
a.sln
dir2
a.csproj
dir21
dir3
b.csproj
a.csproj has output path set to '../../_bin' which is just above dir1 if counted from a.csproj folder
b.csproj has output path set to '../../../_bin' which is same - just about dir1 if counted from b.csproj
a.sln contains both - a.csproj and b.csproj.
When I run msbuild I get a project build to 'dir1/../../_bin' and b project to 'dir1/../../../_bin' - both relative paths of projects files are counted from solution file location, not project files.

Well, I was able to find out what was causing this. That was custom .targets file, which was inferring SolutionDir property at the start of any msbuild.
I did find out that by using MSBuild Explorer. The tool proved to be very useful in my case - I was not aware of third party .target files on my system.

From Msbuild Import Element description
Relative paths in imported projects are interpreted relative to the
directory of the importing project. Therefore, if a project file is
imported into several project files in different locations, the
relative paths in the imported project file will be interpreted
differently for each imported project.
All MSBuild reserved properties that relate to the project file, for example, MSBuildProjectDirectory
and MSBuildProjectFile, that are referenced in an imported project are
assigned values based on the importing project file.
If you add more details or few samples to your question - it will be easier to understand exact problem.
Edit:
Okay, lets try to pinpoint that mystery. First of all - OutputPath could be affected by Environment variables.
2nd - during build sln file transformed into msbuild project file format and stored in temp file. You can get that temporary file if you execute in cmd "set msbuildemitsolution=1" and then trigger build via command line. There you can check that file and see how your individual projects called. But I suppose you will see multiple .csproj /> entries. And global msbuild properties inherited by that calls.
So I suspect if everything was fine before some point and no changes were made - you are missing OutputPath environment variable or some other variable that contributed to construction of OutputPath.
BTW - I think if you want to fix your issue with forcing relative dir - you also can use $(MSBuildProjectDirectory). This is one of msbuild reserved properties (from here), but this will require yo adjust your OutputPath in each csproj file. What i, personally, prefer to avoid, because it could affect some other targets and introduce subtle issues.

Related

How do I configure Visual Studio 2022 to build projects on another drive?

I have a system with a 1 TB SATA-connected SSD as the system disk and a 256 GB M.2 SSD as an auxiliary data disk/scratch drive. I would like to configure Visual Studio 2022 to create all project build directories (but not the projects themselves) inside a folder on this scratch drive (F:\build). From what I can tell, CMake-based projects can achieve this by creating a global CMakeSettings.json template; however, I haven't found anything for MSBuild-based projects. Is it possible to configure the MSBuild defaults to do this?
A folder tree of what I'm trying to do would look a little like this:
F:\
|- foo
|- bar
|- build
|- Project1
|- Project2
MSBuild is not a build script generator like CMake. When you create a project file with Visual Studio or the dotnet tool, the project itself is an MSBuild script. The project file should be source controlled. It is not a 'scratch' file.
Generally, MSBuild projects use an 'intermediate' directory and an 'output' directory. By default, the intermediate directory is obj\ and the output directory is bin\. These defaults can be changed by changing the BaseIntermediateOutputPath and BaseOutputPath properties. (See List of common properties and parameters.)
You can set or change properties globally by using a Directory.Build.props file. (See Customize your build.) The Directory.Build.props file is imported very early which is important because there are numerous properties defined based on the BaseIntermediateOutputPath and BaseOutputPath properties.
You might create a Directory.Build.props file like the following:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$([MSBuild]::GetPathOfFileAbove('$(MSBuildThisFile)', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<Acme-Drive Condition="'$(Acme-Drive)' == ''">F:</Acme-Drive>
<Acme-BuildDir Condition="'$(Acme-BuildDir)' == ''">$(Acme-Drive)\build\</Acme-BuildDir>
<BaseOutputPath Condition="'$(BaseOutputPath)' == ''">$(Acme-BuildDir)$(MSBuildProjectName)\bin\</BaseOutputPath>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)' == ''">$(Acme-BuildDir)$(MSBuildProjectName)\obj\</BaseIntermediateOutputPath>
</PropertyGroup>
</Project>
Some notes on this MSBuild code:
MSBuild will search up the directory structure and will load the first Directory.Build.props file found. The Import will search for and if present will load the next file. It's a good practice to always add the Import when creating a Directory.Build.props file (or a Directory.Build.targets file). The chain of imports will continue to work if Directory.Build.* files are added or removed in the directory tree.
Acme-Drive and Acme-BuildDir are custom properties. 'Acme-' is used as a prefix. The prefix can be anything that is appropriate for your organization or product. The prefix both provides a lower chance of a property name collision and indicates this is a custom property.
The Condition="'$(Acme-Drive)' == ''" tests if the property is unset. The value F: is only set if the Acme-Drive property doesn't already have a value. Properties can be redefined and overridden from other files and from the command line. For example, passing /p:"Acme-Drive=z:" on the MSBuild command line would switch the drive for that one run.
The value of the $(MSBuildProjectName) property is the name of the current project.
If
Your local working directory for the source code is C:\repos\MyProduct.
The code above is saved as C:\repos\MyProduct\Directory.Build.props.
You have project files:
C:\repos\MyProduct\project1\Project1.csproj
C:\repos\MyProduct\project1\Project2.csproj
then
Both projects will use the same Directory.Build.props file.
For Project1, $(BaseOutputPath) will be F:\build\project1\bin.
For Project2, $(BaseOutputPath) will be F:\build\project2\bin.

Visual Studio 2022 ignores BaseIntermediateOutputPath property specified in project files

I have a solution containing a large number of projects.
To separate binaries from the sources, I changed BaseOutputPath to "..\bin" and BaseIntermediateOutputPath property in all projects to "..\obj".
It worked for the BaseOutputPath - all binaries are in the common directory.
For the intermediate path it didn't work at all. It broke the projects, because the obj directory is still created in the project folders, but now the files are not excluded from the project and appear as commitable for Git.
What am I doing wrong? Is it normal behavior? I don't want obj directories created in project directories. How can I achieve that behavior?
For now I just removed the property from projects. At least the obj folders are not shown as project folders.
You need to use MSBuild macros such as $(SolutionDir) or $(MSBuildThisFileDirectory) and similar, if you do not want relative paths to start from your project file.
For example, you could set "BaseIntermediateOutputPath" to $(SolutionDir)\obj or better still to $(SolutionDir)\obj-$(Configuration). This way you will separate Debug and Release builds, which, if you don't, will get you into a lot of trouble.
Here is what I use in a particular "Directory.Build.props". Note the <OutDir> entry. This will change the output directory for all projects under this directory to the stage64\lib directory.
<PropertyGroup>
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
<PlatformToolset>v141</PlatformToolset>
<OutDir>$(SolutionDir)..\stage64\lib\</OutDir>
</PropertyGroup>

How to have msbuild copy files to output directory across references?

Suppose we have a project A with output directory bin/ and a project B with output directory test/bin/. Project A needs to have a certain configuration file copied to its output directory. Currently, this is being done by adding a pre-build event that looks like
COPY /Y "$(ProjectDir)..\config.ini" "$(TargetDir)"
Project B has A as dependency. When project B builds, the binary resulting from A is correctly copied to the output directory of B, but the config file isn't!
How can we achieve that all files that A needs to have are also copied to the output directory of B?
What you want is a Link File. I've used these before to ensure that connection.config files are copied to multiple projects.
I created a sample solution that mimics your described scenario as seen below. The ConsoleApplication1 references the ClassLibrary1 project. In the solution, I created a solution folder config and added the shared config that needs to be copied.
In order to have a single file that is copied across multiple projects, you must create a single file, and then add it as a link to the projects that need it. This can be done by right clicking on the project, and choosing Add -> Existing Item.... You will see a dialog as show below and you want to choose the Add as Link option from the dropdown instead of just Add.
Finally, edit the properties for the linked file to copy it to the output directory.

Output Path and MSBuild

I have a solution with 9 projects. All references in each project have their CopyLocal property set to False.
When I build it from VS, none of these referenced binaries are copyed to the output build directory. Similarly, when I build using msbuild, I only see the project binaries and no references.
However, when I specify an output path in the msbuild command, some references are copied and I don't know why? Is there some setting I am forgetting to set? Has anyone seen this before?
When you build your application without setting the OutDir property, the files are copied to the path specified in project properties (Build\Output path). After this, there is another step that copies the referenced project output (*.dll file) to the OutDir of your application (*.exe). But if you set CopyLocal to false, this last step doesn't happen. Like this:
ProjectEXE\OutDir = c:\a\bin
ProjectDLL\OutDir = c:\b\bin
Pay attention to the fact that the two OutDir are different (and differently specified in your projects properties).
But when you set the OutDir using the command-prompt, you are setting both OutDir parameters to the same path. Still there is no final copy of the DLL to the same directory of your application, except for the fact that it is the same location you built the DLL and EXE first time. Like this:
msbuild yourSolution.sln /p:OutDir="c:\a\bin\"
ProjectEXE\OutDir = c:\a\bin
ProjectDLL\OutDir = c:\a\bin

What is incremental clean in msbuild and when is it triggered?

I am debugging a bug in my build process that happens occasionally but I can't directly reproduce it. I'm using msbuild with teamcity.
I have a dependency hierarchy like this:
Some.Interop.dll
Dependency-> SharedDllABC.dll
SomeService.exe
Depenendcy-> Some.Interop
Usually the final service exectuable gets in its release directory:
Some.Interop
SharedDllABC.Dll
ServiceExectuable.exe
However I can see in our msbuild logs that sometimes the tertiary dependency gets deleted during an Incremental Clean after everything is built resulting in:
Some.Interop
ServiceExectuable.exe
You can see it here in the msbuild log:
[src\SomeService\SomeService.csproj] _TimeStampAfterCompile
[12:32:43]: [src\SomeService\SomeService.csproj] Compile
// some other targets
[12:32:43]: [src\SomeService\SomeService.csproj] _CopyFilesMarkedCopyLocal
[12:32:43]: [_CopyFilesMarkedCopyLocal] Copy
[12:32:43]: [Copy] Copying file from "C:Projects\trunk\src\Some.Interop\bin\Release\Some.Interop.dll" to "bin\Release\Some.Interop.dll".
// some other targets
[src\Project\SomeService\SomeService.csproj] IncrementalClean
[18:54:42]: [IncrementalClean] Delete
[18:54:42]: [Delete] Deleting file "C:\Projects\trunk\src\Project\SomeService\bin\Release\SharedDllABC.dll".
[18:54:42]: [Delete] Deleting file "C:\Projects\trunk\src\Project\SomeServiceService\bin\Release\SharedDllABC.pdb".
[18:54:42]: [src\Project\SomeService\SomeService.csproj] CoreBuild
[18:54:42]: [src\Project\SomeService\SomeService.csproj] AfterBuild
[18:54:42]: [src\Project\SomeService\SomeService.csproj] Build
This is my direct msbuild output, I just changed the project names/dll names to match my example. By the time this Incremental Clean has occurred the SomeService.csproj has already been built. You can see that its not getting copied. However in other msbuild logs it does properly get copied and then the incremental clean doesn't delete it.
I think incrementeal clean from this post is supposed to clean dll's that were created from previous builds, but that doesn't explain how this dll didn't get built when most of the time it does. In visual studio this always works as well.
I guess I just want to know what exactly is Incremental clean, what causes it to kick in, and maybe what things I should look for when debugging a situation like this (assembly versions, timestamps, etc?)
Try the following:
Add:
<Target Name="IncrementalClean" />
to a .targets file that's included in all projects.
From --> https://github.com/Microsoft/msbuild/issues/1054
#Kebabbi recommends a good fix by editing a csproj file. As of MSBuild 15, there is a simple way to make this apply to all CSPROJ files, instead of editing each csproj file.
https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-your-build?view=vs-2017
Directory.Build.props and Directory.Build.targets
Prior to MSBuild version 15, if you wanted to provide a new, custom property to projects in your solution, you had to manually add a reference to that property to every project file in the solution. Or, you had to define the property in a .props file and then explicitly import the .props file in every project in the solution, among other things.
However, now you can add a new property to every project in one step by defining it in a single file called Directory.Build.props in the root folder that contains your source. When MSBuild runs, Microsoft.Common.props searches your directory structure for the Directory.Build.props file (and Microsoft.Common.targets looks for Directory.Build.targets). If it finds one, it imports the property. Directory.Build.props is a user-defined file that provides customizations to projects under a directory.
Create a file Directory.Build.props, and place it adjacent to the SLN file.
<Project>
<Target
Name="ForceAssignProjectConfigurationBeforeSplitProjectReferencesByFileExistence_KLUDGE"
BeforeTargets="_SplitProjectReferencesByFileExistence"
DependsOnTargets="AssignProjectConfiguration" />
</Project>
This could be caused by a bug in MsBuild: https://github.com/Microsoft/msbuild/issues/1054.
A fix is proposed in the comments: https://github.com/Microsoft/msbuild/issues/1054#issuecomment-406438561
When MsBuild determines which items to copy from referenced projects, it should do this recursively but does not properly do this.
As a workaround the following can be added to each csproj.
<Target
Name="ForceAssignProjectConfigurationBeforeSplitProjectReferencesByFileExistence_KLUDGE"
BeforeTargets="_SplitProjectReferencesByFileExistence"
DependsOnTargets="AssignProjectConfiguration"
/>
I just spent a few days trying to figure this out with a similar pattern. In our case it was nuget files that were being removed from the output folder.
NugetPackage (that drops files in x86/x64 subfolders in output folder)
LibraryA.dll
Dependency-> NugetPackage
LibraryB.dll
Dependency-> LibraryA.dll
In our case, we have a number of solution files that are built as part of an msbuild script in a certain order.
The problem was that LibraryB.csproj was included in two solution files.
Solution1 builds and output files are all present.
Solution2 builds and sees that LibraryB.dll is present and up to date, so for some reason triggers the IncrementalClean that removes the NugetPackage files from the output folder.
Once I removed the LibraryB.csproj from solution 2, the problem is solved and the files are present in the output folder.

Resources