2

Since I started using MSBuild for our projects, I've created several .proj scripts that are shared by several projects in our repository. All these shared scripts reside in a single directory.

So far I've been referring to the shared scripts by using a relative path, something like this:

<MSBuild Projects="..\..\common\build\MyScriptA.proj" Properties="ABC=XYZ"/>

However, every project also imports a common .proj script like so:

<Import Project="..\..\common\build\CommonImports.proj"/>

which <Import>s several other things and defines some properties.

This morning I thought I could replace the relative path with a variable, perhaps $(CommonDir), which would be defined by importing the CommonImports.proj mentioned above. This would enable me to call the common tasks like this:

<MSBuild Projects="$(CommonDir)\MyScriptA.proj" Properties="ABC=XYZ"/>

However, I can't figure out a way to define this $(CommonDir) variable in such a way as to make it work in all other MSBuild scripts that import CommonImports.proj, regardless of their location.

This question offers several ways of creating a property containing an absolute path from a relative path, but none of those seem to work if all I do is <Import> the script defining the property.

Question 1: I'm fairly new to MSBuild; is there a better way of creating a "library" of reusable .proj scripts I could run via the <MSBuild> task? I am aware of $(MSBuildExtensionsPath), however I would like the common tasks to reside in my checkout so that our build machine would automatically get the latest versions of the common tasks whenever it performs a checkout.

Question 2: How do I define $(CommonDir) inside CommonImports.proj so as to make it contain the absolute path to the directory containing CommonImports.proj?

Community
  • 1
  • 1
Roman Starkov
  • 59,298
  • 38
  • 251
  • 324
  • How are you calling MSBuild? Is that the NAnt Task? If you're not using NAnt, could you use that to assist you? If you are using NAnt, ignore me completely. – Noon Silk Aug 24 '09 at 11:34
  • 1
    Not using NAnt, and not looking for a solution that would use NAnt. – Roman Starkov Aug 24 '09 at 21:23

4 Answers4

2

I wonder if you shouldn't put it in the MSBuild path:

For example, this project is consumed via:

<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

and installs itself into:

{program files}\MSBuild\MSBuildCommunityTasks

So perhaps define your own specific sub-folder, and use from there?

<Import Project="$(MSBuildExtensionsPath)\romkyns\CommonImports.proj"/>

etc. Because the $MSBuildExtensionsPath variable is defined separately you shouldn't have as much difficulty with it. Maybe.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks, indeed I noticed that msbuildtasks installs itself there. However our common tasks change more frequently (for now at least) than msbuildtasks - which is why I'm looking for a way to let the tasks be referenced within the checkout. As a bonus, if I sync back to the past and invoke a build I will be using the same common tasks I did when a release was made, with no extra hassle. – Roman Starkov Aug 24 '09 at 19:22
2

This isn't a detailed answer, but here's some notes that are generally very useful when people solve this problem. All require MSBuild 4.0 or later.

(1) The $(MSBuildThisFile) property and similar properties. It allows imported files to refer to files relative to themselves, rather than to the project they're imported in. That uncouples them from the project they're imported into.

(2) The "GetDirectoryNameOfFileAbove" function. See here. This wonderfully useful function makes it possible for projects to import files whose location they don't know. Put the shared files (or one single stub shared file that imports the others from somewhere) at the top of your source tree. Then use this function in projects below it to find that stub and import it and thus all your shared build process.

(3) Import tag allows wildcards. This makes it possible to cause a file to be imported -- in other words, extend a build process -- by simply dropping it in a particular location, and editing no existing files.

cheerless bog
  • 914
  • 8
  • 11
  • All 3 of these are great suggestions. And now we have Directory.Build.props (and Directory.Build.targets) as well for automatic importing: https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-your-build?view=vs-2022 – Arin Taylor Mar 21 '22 at 21:11
1

Here's what's worked best for us.

First, check in all reusable stuff into a /common/build/ directory in the VCS.

Then, add a /common/build/CommonImports.proj, looking something like this:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="4.0">
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
  <Import Project="ILMerge.proj"/>
  <Import Project="Mage.proj"/>
  <Import Project="InnoSetup.proj"/>
  <UsingTask TaskName="RT.Tasks.AssemblyVersion" AssemblyFile="Tasks\Release\RT.Tasks.dll"/>
  <UsingTask TaskName="RT.Tasks.WaitForProcessesToTerminate" AssemblyFile="Tasks\Release\RT.Tasks.dll"/>
  <PropertyGroup>
    <BuildSingleProj>$(Root)\common\build\BuildSingle.proj</BuildSingleProj>
  </PropertyGroup>
</Project>

This imports a few projects and tasks that everything uses, and defines a property group that is shared by everything. Then, add this in every build script:

<PropertyGroup>
  <Root>..\..</Root>
  [... other global properties ...]
</PropertyGroup>
<Import Project="$(Root)\common\build\CommonImports.proj"/>
Roman Starkov
  • 59,298
  • 38
  • 251
  • 324
0

10+ years later, another strategy is to put a Directory.Build.props file in the root of the repository declaring a property using $(MSBuildThisFileDirectory) to get the path to the root of the repository (or other useful place). Then all projects in the repository can use paths relative to the root instead of to themselves.

I would probably only do this if I don't mind an extra file at the root of the repository, and if the repository primarily consists of MSBuildable projects.

Example:

/Directory.Build.props
/common/build/MyScriptA.proj
/common/build/CommonImports.proj

Directory.Build.props:

<Project>
  <PropertyGroup>
    <RepoRootDir>$(MSBuildThisFileDirectory)</RepoRootDir>
    <CommonDir>$(RepoRootDir)common\build\</CommonDir>
  </PropertyGroup>
</Project>

Note that if you use nested Directory.Build.props files, they would need to import upwards using something like this: <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> since it stops after finding the first match.

Arin Taylor
  • 380
  • 1
  • 7