Sunday, June 6, 2010

How to build an F# library for the Xbox 360 using msbuild

In earlier post I described the difficulties of building F# libraries that will run on the Xbox 360.
I have been thinking all along that this was a bug in the integration of F# in Visual Studio, but I realized yesterday that it may be otherwise. Out of curiosity, I investigated how customization of msbuild works.
If you create a C# XNA project and open the csproj file, you should see these lines at the end:

<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath)\Microsoft\XNA Game Studio\Microsoft.Xna.GameStudio.targets" />

If you go ahead and open Microsoft.Xna.GameStudio.Common.targets (located in C:\Program Files\MSBuild\Microsoft\Xna Game Studio\v3.1\), you will find some code dedicated to resolving assembly references:

The SearchPaths property is set to find assemblies in the following order:

(1) Files from current project - indicated by {CandidateAssemblyFiles}
(2) $(ReferencePath) - the reference path property, which comes from the .USER file.
(3) The hintpath from the referenced item itself, indicated by {HintPathFromItem}.
(4) The directory of MSBuild's "target" runtime from GetFrameworkPath.
The "target" runtime folder is the folder of the runtime that MSBuild is a part of.
(5) Registered assembly folders, indicated by {Registry:*,*,*}
(6) Legacy registered assembly folders, indicated by {AssemblyFolders}
(7) Look in the application's output folder (like bin\debug)
(8) Resolve to the GAC.
(9) Treat the reference's Include as if it were a real file name.
<AssemblySearchPaths Condition=" '$(XnaPlatform)' != 'Windows' ">

I don't quite understand exactly what happens in these targets files, but it seems the XNA dev team had to do some magic to have assembly resolution work.
From there, I got the idea to import the XNA targets files in an F# project and see if it works. I tried that, mixing a C# XNA library project file with an F# library project. Guess what? It worked!

To summarize, here are the steps to build and F# XNA library project:
1. Make a copy of your F# library project, naming it "Xbox 360 Copy of my project.fsproj"
2. Find the section which sets the default platform, change it from AnyCPU to Xbox 360:

<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
- <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <Platform Condition=" '$(Platform)' == '' ">Xbox 360</Platform>

3. Under the target framework version, add some specific XNA properties:

+ <XnaFrameworkVersion>v3.1</XnaFrameworkVersion>
+ <XnaPlatform>Xbox 360</XnaPlatform>

4. Find the two sections controlling the project settings depending on the configuration and platform. Change AnyCPU to Xbox 360, change the output directory, add some constants:

- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|Xbox 360' ">
- <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <DefineConstants>DEBUG;TRACE;XBOX;XBOX360</DefineConstants>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|Xbox 360' ">
- <OutputPath>bin\Release\</OutputPath>
- <DefineConstants>TRACE</DefineConstants>
+ <OutputPath>bin\Xbox 360\Release\</OutputPath>
+ <DefineConstants>TRACE;XBOX;XBOX360</DefineConstants>

5. Remove the sections dedicated to the x86 platform.

- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
- <DebugSymbols>true</DebugSymbols>
- <DebugType>full</DebugType>
- <Optimize>false</Optimize>
- <Tailcalls>false</Tailcalls>
- <OutputPath>bin\Debug\</OutputPath>
- <DefineConstants>DEBUG;TRACE</DefineConstants>
- <WarningLevel>3</WarningLevel>
- <PlatformTarget>x86</PlatformTarget>
- </PropertyGroup>
- <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
- <DebugType>pdbonly</DebugType>
- <Optimize>true</Optimize>
- <Tailcalls>true</Tailcalls>
- <OutputPath>bin\Release\</OutputPath>
- <DefineConstants>TRACE</DefineConstants>
- <WarningLevel>3</WarningLevel>
- <PlatformTarget>x86</PlatformTarget>
- </PropertyGroup>

6. Add some XNA-specific stuff (no idea what it does, not sure if it's needed):

+ <ItemGroup>
+ <BootstrapperPackage Include="Microsoft.Net.Framework.2.0">
+ <Visible>False</Visible>
+ <ProductName>.NET Framework 2.0 %28x86%29</ProductName>
+ <Install>false</Install>
+ </BootstrapperPackage>
+ <BootstrapperPackage Include="Microsoft.Net.Framework.3.0">
+ <Visible>False</Visible>
+ <ProductName>.NET Framework 3.0 %28x86%29</ProductName>
+ <Install>false</Install>
+ </BootstrapperPackage>
+ <BootstrapperPackage Include="Microsoft.Net.Framework.3.5">
+ <Visible>False</Visible>
+ <ProductName>.NET Framework 3.5</ProductName>
+ <Install>true</Install>
+ </BootstrapperPackage>
+ <BootstrapperPackage Include="Microsoft.Windows.Installer.3.1">
+ <Visible>False</Visible>
+ <ProductName>Windows Installer 3.1</ProductName>
+ <Install>true</Install>
+ </BootstrapperPackage>
+ <BootstrapperPackage Include="Microsoft.Xna.Framework.3.1">
+ <Visible>False</Visible>
+ <ProductName>Microsoft XNA Framework Redistributable 3.1</ProductName>
+ <Install>true</Install>
+ </BootstrapperPackage>
+ </ItemGroup>

7. Fix references:

<Reference Include="Microsoft.Xna.Framework">
- <HintPath>..\..\..\..\..\..\..\..\..\Program Files\Microsoft XNA\XNA Game Studio\v3.1\References\Windows\x86\Microsoft.Xna.Framework.dll</HintPath>
+ <Private>False</Private>
<Reference Include="Microsoft.Xna.Framework.Game">
- <HintPath>..\..\..\..\..\..\..\..\..\Program Files\Microsoft XNA\XNA Game Studio\v3.1\References\Windows\x86\Microsoft.Xna.Framework.Game.dll</HintPath>
+ <Private>False</Private>

8. Insert the C# and XNA target files, before the F# targets file:

+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <Import Project="$(MSBuildExtensionsPath)\Microsoft\XNA Game Studio\Microsoft.Xna.GameStudio.targets" />
<Import Project="$(MSBuildExtensionsPath32)\FSharp\1.0\Microsoft.FSharp.Targets" />

You are almost there. You now have a project file you can open in Visual Studio. From there, you can add a reference to the correct FSharp.Core assembly (the one for .Net CF 2.0)

That should be it. There are a few more things that need to be fixed, such as project GUIDs, but Visual Studio will happily do that for you, all you have to do is open the fsproj file with Visual Studio.

When building, remember to choose "Xbox 360" as the platform in the build settings.