I am using MSBuild Community Tasks to run Xsd.exe as part of my build in Visual Studio as thus:
<Import Project="$(SolutionDir)Common.targets" />
<PropertyGroup>
<MSBuildCommunityTasksPath>$(SolutionDir)TeamBuildTypes</MSBuildCommunityTasksPath>
</PropertyGroup>
<Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets" />
<UsingTask TaskName="XSD" AssemblyFile="$(VCTargetsPath)$(CPPTasks)" />
<Target Name="BeforeBuild">
<!--Exec Command="'$(DevEnvDir)..\Tools\vsvars32.bat'" /-->
<XSD Sources="MySchema.xsd" GenerateFromSchema="classes" Language="CS" />
</Target>
Common.targets is as per here.
However, I get the following error during build:
The "XSD" task failed unexpectedly.
Microsoft.Build.Shared.InternalErrorException: MSB0001: Internal MSBuild Error: xsd.exe unexpectedly not a rooted path
A solution has been given on MSDN forum here, which is to add a path to xsd.exe to the path environment variable. But, as indicated by Pico Ohms' answer, this is not maintainable as it is version-dependent and requires every dev to perform an extra step in order to build, just because of Xsd.exe.
I found another solution here which is to call vsvars32.bat beforehand. This is the commented-out line in the code above. This didn't work, so I found a solution here that I hoped would make it work, which was to call DEVENV with the /useenv
parameter.
Then I found another solution here which is to add <xs:include schemaLocation="MSBuild\MSBuild.Community.Tasks.xsd"/>
to Microsoft.Build.xsd. Didn't work either.
So, now I'm out of ideas. How do I get the MSBuild Community Tasks XSD task working without requiring devs to update their path variable, either by using the /useenv
solution, or some other solution? I'm aware I can put xsd.exe in the Visual Studio solution, but this seems like a cheap workaround to me.
I had to do some research to remember the comment I posted on the other thread about this, but you can implement a custom search path for your task by deriving from the XSD task and overriding the GenerateFullPathToTool() method.
When you look at the XSD task in DotPeek you can see it derives from Microsoft.Build.Utilities.ToolTask. ToolTask contains a method called "GenerateFullPathToTool()". This method is called to return the full path to the tool as its name implies.
The ToolLocationHelper in Microsoft.Build.Utilities class can be used to find the appropriate location for your installed xsd.exe.
It contains a method called GetPathToDotNetFrameworkSdkFile which will give you the location of fileName for the dot net sdk. Put "xsd.exe" as the parameter and you should be good to go.
If that method doesn't return the path you need, there are other methods for returning the various paths and its based off of the registry so its more portable than setting an explicit path.
A solution for this that I have been using for many years is to use an inline task to set the path within the project file.
<ItemGroup>
<!-- Configure XSD files by using the XsdClasses custom item type. All files of this type are converted by the GenerateXsdClasses target. -->
<XsdClasses Include="Data\XsdFile1.xsd">
<SubType>Designer</SubType>
</XsdClasses>
<XsdClasses Include="Data\XsdFile2.xsd">
<SubType>Designer</SubType>
</XsdClasses>
</ItemGroup>
<!-- In-line task to add a folder to the current path if not already in it. -->
<UsingTask TaskName="AddToPath" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<FolderPath ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="c#"><![CDATA[
const string PathEnvironmentVariableName = "PATH";
string currentPath = Environment.GetEnvironmentVariable(PathEnvironmentVariableName);
if(!currentPath.ToLower().Contains(FolderPath.ToLower()))
{
currentPath = currentPath.TrimEnd(';');
string newPath = string.Format("{0};{1}", currentPath, FolderPath);
Environment.SetEnvironmentVariable(PathEnvironmentVariableName, newPath);
}
]]></Code>
</Task>
</UsingTask>
<!-- Reference the Xsd task. -->
<UsingTask TaskName="XSD" AssemblyFile="$(VCTargetsPath)Microsoft.Build.CppTasks.Common.dll" />
<!-- Set the default path to Xsd.exe - this will be added to the current path using the above inline task. Can be overridden per client by setting a user or machine environment variable with the same name. -->
<PropertyGroup>
<NetFxToolsPath Condition="'$(NetFxToolsPath)' == ''">C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7 Tools\</NetFxToolsPath>
</PropertyGroup>
<!-- Generate classes from xsd files configured with 'XsdClasses' item type. -->
<Target Name="GenerateXsdClasses" Inputs="@(XsdClasses)" Outputs="@(XsdClasses->'%(RelativeDir)%(Filename).cs')">
<Message Importance="high" Text="Building %(XsdClasses.Filename)" />
<AddToPath FolderPath="$(NetFxToolsPath)" />
<!-- Note the trailing . in the outputdir because the folder ends with a / -->
<XSD MinimalRebuildFromTracking="true" Sources="%(XsdClasses.Identity)" GenerateFromSchema="classes" Namespace="XsdClassesNamespace" SuppressStartupBanner="true" AdditionalOptions="/outputdir:"%(XsdClasses.RelativeDir)."" />
</Target>
<!-- Configure class generation from xsd to occur before build. -->
<Target Name="BeforeBuild" DependsOnTargets="GenerateXsdClasses"/>