Find a file in the parent folders with msbuild
Asked Answered
C

2

5

In MsBuild it is possible to create a build.proj.user file which is parsed by the Microsoft.Common.Targets build file.

I want to have a similar system in place where it is possible to have a .user file in the root of the folder and make msbuild pick up the config settings from this file.

Take for example these paths:

c:\working\build.proj.user
c:\working\solution1\build.proj.user
c:\working\solution1\project1\
c:\working\solution1\project2\
c:\working\solution1\project3\build.proj.user

c:\working\solution2\
c:\working\solution2\project1\
c:\working\solution2\project2\

I want to achieve that for solution1/project1 the file c:\working\solution1\build.proj.user is read and for solution2/project1 the file c:\working\build.proj.user

The purpose is to allow integration test connectionstring properties to be customized per solution and or project.

The current solutions I see are:

  • Make a custom msbuild task which will go look for this file
  • Construct a shell command to find the file.
  • Have it hard-coded look in the parent and parent of parent path

I am not a fan of either solution and wonder if there isn't a more elegant way of achieving my goal (with msbuild).

Creditor answered 23/11, 2011 at 11:31 Comment(0)
R
9

Add this to your project files:

<Import Project="build.proj.user" Condition="Exists('build.proj.user')"/>
<Import Project="..\build.proj.user" Condition="!Exists('build.proj.user') and Exists('..\build.proj.user')"/>
<Import Project="..\..\build.proj.user" Condition="!Exists('build.proj.user') and !Exists('..\build.proj.user') and Exists('..\..\build.proj.user')"/>

EDIT: You can also do it using MsBuild inline task. It is little bit slower, but more generic :) Inline tasks are supported from MsBuild 4.0

<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="4.0">
  <UsingTask TaskName="FindUserFile" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <CurrentDirName ParameterType="System.String" Required="true" />
      <FileToFind ParameterType="System.String" Required="true" />
      <UserFileName ParameterType="System.String" Output="true" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.IO"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          Log.LogMessage("FindUserFile parameters:");
          Log.LogMessage("CurrentDirName = " + CurrentDirName);
          Log.LogMessage("FileToFind = " + FileToFind);

          while(CurrentDirName != Directory.GetDirectoryRoot(CurrentDirName) && !File.Exists(CurrentDirName + Path.DirectorySeparatorChar + FileToFind))
             CurrentDirName = Directory.GetParent(CurrentDirName).FullName;
          if(File.Exists(CurrentDirName + Path.DirectorySeparatorChar + FileToFind)) 
             UserFileName = CurrentDirName + Path.DirectorySeparatorChar + FileToFind;

          Log.LogMessage("FindUserFile output properties:");
          Log.LogMessage("UserFileName = " + UserFileName);
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <Target Name="FindUserFileTest" >
    <FindUserFile CurrentDirName="$(MSBuildThisFileDirectory)" FileToFind="build.proj.user">
     <Output PropertyName="UserFileName" TaskParameter="UserFileName" />
    </FindUserFile>

    <Message Text="UserFileName = $(UserFileName)"/>
    <Error Condition="!Exists('$(UserFileName)')" Text="File not found!"/>

  </Target>
</Project>

How it works: FindUserFile is inline task written in C# language. It tries to find file specified in FileToFind parameter. Then iterate trough all parent folders and it returns the first occurrence of the FileToFind file in UserFileName output property. UserFileName output property is empty string if file was not found.

Rerun answered 23/11, 2011 at 12:2 Comment(3)
This is a solution to the problem, but I was looking for a more generic solution where msbuild would keep looking upwards until the root of the drive.Creditor
Just add few more levels down deep. Unless you are in a habit of creating 50 levels deep directories for your project, that should solve your problem. I agree, it is not the most elegant way of doing that, but in this case custom task will not help. Tasks are executed after all <Import> files are resolved.Hasten
Thanks for the suggestions, I will use the first one until we can use MsBuild 4.0Creditor
B
18

This functionality exists in MSBuild 4.0: $([MSBuild]::GetDirectoryNameOfFileAbove(directory, filename)

Example: include a file named "Common.targets" in an ancestor directory

<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Common.targets))\Common.targets" 
    Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Common.targets))' != '' " />

See this blog post for more details: MSBuild Property Functions

Burushaski answered 30/4, 2012 at 19:28 Comment(0)
R
9

Add this to your project files:

<Import Project="build.proj.user" Condition="Exists('build.proj.user')"/>
<Import Project="..\build.proj.user" Condition="!Exists('build.proj.user') and Exists('..\build.proj.user')"/>
<Import Project="..\..\build.proj.user" Condition="!Exists('build.proj.user') and !Exists('..\build.proj.user') and Exists('..\..\build.proj.user')"/>

EDIT: You can also do it using MsBuild inline task. It is little bit slower, but more generic :) Inline tasks are supported from MsBuild 4.0

<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="4.0">
  <UsingTask TaskName="FindUserFile" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <CurrentDirName ParameterType="System.String" Required="true" />
      <FileToFind ParameterType="System.String" Required="true" />
      <UserFileName ParameterType="System.String" Output="true" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.IO"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          Log.LogMessage("FindUserFile parameters:");
          Log.LogMessage("CurrentDirName = " + CurrentDirName);
          Log.LogMessage("FileToFind = " + FileToFind);

          while(CurrentDirName != Directory.GetDirectoryRoot(CurrentDirName) && !File.Exists(CurrentDirName + Path.DirectorySeparatorChar + FileToFind))
             CurrentDirName = Directory.GetParent(CurrentDirName).FullName;
          if(File.Exists(CurrentDirName + Path.DirectorySeparatorChar + FileToFind)) 
             UserFileName = CurrentDirName + Path.DirectorySeparatorChar + FileToFind;

          Log.LogMessage("FindUserFile output properties:");
          Log.LogMessage("UserFileName = " + UserFileName);
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <Target Name="FindUserFileTest" >
    <FindUserFile CurrentDirName="$(MSBuildThisFileDirectory)" FileToFind="build.proj.user">
     <Output PropertyName="UserFileName" TaskParameter="UserFileName" />
    </FindUserFile>

    <Message Text="UserFileName = $(UserFileName)"/>
    <Error Condition="!Exists('$(UserFileName)')" Text="File not found!"/>

  </Target>
</Project>

How it works: FindUserFile is inline task written in C# language. It tries to find file specified in FileToFind parameter. Then iterate trough all parent folders and it returns the first occurrence of the FileToFind file in UserFileName output property. UserFileName output property is empty string if file was not found.

Rerun answered 23/11, 2011 at 12:2 Comment(3)
This is a solution to the problem, but I was looking for a more generic solution where msbuild would keep looking upwards until the root of the drive.Creditor
Just add few more levels down deep. Unless you are in a habit of creating 50 levels deep directories for your project, that should solve your problem. I agree, it is not the most elegant way of doing that, but in this case custom task will not help. Tasks are executed after all <Import> files are resolved.Hasten
Thanks for the suggestions, I will use the first one until we can use MsBuild 4.0Creditor

© 2022 - 2024 — McMap. All rights reserved.