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

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.

Related

Disable append Configuration Name to Output Folder in vcproj

In a C# csproj project, AppendTargetFrameworkToOutputPath and AppendRuntimeIdentifierToOutputPath prevent msbuild from creating subfolders for target framework and runtime in the build output directory. However, the configuration name is still appended.
Is there a configuration option to prevent a separate subfolder for each configuration?
So I figured out a solution to this. When editing the project settings in Visual Studio, it modifies the <BaseOutputPath> element in the XML project file. Simply change the element name to <OutputPath> instead, and it won't append the configuration name (and as you said, add <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> and <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> to disable appending target framework and runtime identifier).
As an example, I have the following in a <PropertyGroup> in a C# project:
<OutputPath>$(SolutionDir)Build\$(Configuration)\Plugins</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
For a debug build, this will output the build files to <SolutionDir>\Build\Debug\Plugins.

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>

Prevent duplicating files in NuGet content and contentFiles folders

My NuGet package needs to deliver some rather large files to build output directory.
In an old NuGet model, such files have to be stored in content folder of the .nupkg. While in a new model introduced in NuGet 3.3, such files have to be stored in contentFiles folder.
To maintain a compatibility with older versions of NuGet and mainly with Package.config package management format, I need to duplicate the files into both folders. That unfortunately almost doubles a size of the package.
Is there a way to prevent that? Can I somehow link contentFiles to content folder?
Found updated documentation describing this in detail at
MSBuild targets for NuGet.
By default, everything gets added to the root of the content and contentFiles\any\<target_framework> folder within a package and preserves the relative folder structure, unless you specify a package path:
<Content Include="..\win7-x64\libuv.txt">
<Pack>true</Pack>
<PackagePath>content\myfiles\</PackagePath>
</Content>
If you want to copy all your content to only a specific root folder(s) (instead of content and contentFiles both), you can use the MSBuild property ContentTargetFolders, which defaults to "content;contentFiles" but can be set to any other folder names. Note that just specifying "contentFiles" in ContentTargetFolders puts files under contentFiles\any\<target_framework> or contentFiles\<language>\<target_framework> based on buildAction.
If you only want to output the file to the build output (content only copies the file to the output directory but does cause it to be set as copy to output directory item), you can use a completely different approach by creating an msbuild file that will be included in the project.
You can do this by putting both the file - say test.jpg into the tools folder (you could also use build) and add a Your.Package.Id.targets file to the build folder (the name being the package id of your package with .targets as extension) with the following content:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Content Include="$(MSBuildThisFileDirectory)..\tools\test.jpg">
<Link>test.jpg</Link>
<Visible>false</Visible>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>
This target will be automatically imported into the project files regardless of which "style" of NuGet reference is used (packages.config, PackageReference) and should be backwards compatible to older versions of VS as long as they support NuGet and ToolsVersion 4.0.
The Link metadata denotes where in the output / publish directories the file will end up. You could set it to e.g. defaultContent\images\foo.jpg to create a nested structure and rename the file. (you could even use MSBulid variables to use some of the referencing project's configuration). The Visible metadata prevents the solution explorer from showing the full relative path to the file, which could end up in lots of nested .. nodes. The CopyToPublishDirectory applies to .NET Core / ASP.NET Core apps or SDK-based projects using the Publish target for publishing.
Note that you can set the Inclue-path to anything depending on where in your package the file is. You can also use wildcards (but then set Link to %(Filename)%(Extension))

Relative paths with MSBuild project vs solution

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.

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