To properly reference a tool like sn
or sqlmetal
(what I am after) in an msbuild script in the way that will work for the most people, you must take into consideration different aspects of the operating environment and framework implementation. There are two main cases: Microsoft Windows and Microsoft’s implementation of the framework followed by everything else (by which I mean Mono/unix). An example of the correct approach which supports the situations I can think of is listed at the end.
Microsoft
The proper way to find where sn
or other similar tools live in Windows is to start with the GetFrameworkSdkPath task, as already mentioned.
However, as the question suggests, the exact location within the FrameworkSdkPath that sn
or another tool lives cannot be determined directly. The referenced answer suggests that the only possible folders under FrameworkSdkPath for tools to reside in are bin
and bin/NETFX 4.0 Tools
. However, other values are possible (Visual Studio 2013 Preview uses bin/NETFX 4.5.1 Tools
). Thus, the only proper way to search for sn
is to use a glob expression or recursively search for it. I have trouble figuring out how to do glob expansion with MSBuild and the built-in MSBuild tasks do not seem to support searching under FrameworkSdkPath for particular utilities. However, cmd’s WHERE
has this functionality and can be used to do the search. The result is something like the following msbuild code:
<Target Name="GetSNPath" BeforeTargets="AfterBuild">
<GetFrameworkSdkPath>
<Output TaskParameter="Path" PropertyName="WindowsSdkPath" />
</GetFrameworkSdkPath>
<Exec Command="WHERE /r "$(WindowsSdkPath.TrimEnd('\\'))" sn > sn-path.txt" />
<ReadLinesFromFile File="sn-path.txt">
<Output TaskParameter="Lines" PropertyName="SNPath"/>
</ReadLinesFromFile>
<Delete Files="sn-path.txt" />
<PropertyGroup>
<SNPath>$([System.Text.RegularExpressions.Regex]::Replace('$(SNPath)', ';.*', ''))</SNPath>
</PropertyGroup>
</Target>
(See Property Functions to see why I can use String.TrimEnd
here. WHERE
doesn’t like trailing slashes. EDIT: I added use of Property Functions to access Regex.Replace()
to delete all but the first found path in the SNPath
property. One of my friend’s machines’s WHERE
invocations would output multiple results for certain commands and broke any attempt to <Exec/>
the fond tool. This change ensures that only one result is found and that <Exec/>
s actually succeed.)
Now you can invoke sn
with <Exec Command=""$(SNPath)"" />
.
Portable
Unsurprisingly, resolving the path to sn
is much simpler on any operating system other than Windows. On Mac OSX and any distribution of Linux, I find sn
in the PATH. Using GetFrameworkSdkPath
does not help in such a situation; in fact, this seems to return a path under which sn
cannot be found, at least for the old versions of mono-2.10 I tested while using xbuild:
- On Mac OSX
FrameworkSdkPath
is /Library/Frameworks/Mono.framework/Versions/2.10.5/lib/mono/2.0
and /usr/bin/sn
is a symlink to /Library/Frameworks/Mono.framework/Commands/sn
.
- On a certain Linux install,
FrameworkSdkPath
is /usr/lib64/mono/2.0
and sn
is /usr/bin/sn
(which is a shell script invoking /usr/lib64/mono/4.0/sn.exe
with mono
).
Thus, all we need to do is try to execute sn
. Any unix users placing their sn
implementations in non-standard places already know to update PATH appropriately, so the build script has no need to ever search for it. Also, WHERE
does not exist in unix. Thus, in the unix case, we want to replace the first <Exec/>
call with something that will output just sn
on unix and still do the full search when run on Windows. To differentiate unix-like and Windows environments, we use a trick which takes advantage of unix shells’ shortcut for the true
commmand and cmd’s label syntax. As a short example, the following script will output I’m unix!
in a unix shellout and I’m Windows :-/
on a Windows shellout.
:; echo 'I’m unix!'; exit $?
echo I’m Windows :-/
Taking advantage of this, our resulting GetSNPath
Task looks like:
<Target Name="GetSNPath" BeforeTargets="AfterBuild">
<GetFrameworkSdkPath>
<Output TaskParameter="Path" PropertyName="WindowsSdkPath" />
</GetFrameworkSdkPath>
<Exec Command=":; echo sn > sn-path.txt; exit $?
WHERE /r "$(WindowsSdkPath.TrimEnd('\\'))" sn > sn-path.txt" />
<ReadLinesFromFile File="sn-path.txt">
<Output TaskParameter="Lines" PropertyName="SNPath"/>
</ReadLinesFromFile>
<Delete Files="sn-path.txt" />
<PropertyGroup>
<SNPath>$([System.Text.RegularExpressions.Regex]::Replace('$(SNPath)', ';.*', ''))</SNPath>
</PropertyGroup>
</Target>
The result is a portable method for finding the string required to invoke sn
. This last solution lets you support both Microsoft and its msbuild and every other platform using xbuild. It also overcomes hardcoding bin\NETFX 4.0 Tools
into .csproj files to support future and current versions of Microsoft tools simultaneously.