hubFS: THE place for F#

. . . are you on The Hub?
Welcome to hubFS: THE place for F# Sign in | Join | Help
in Search

Msbuild task in F#

Last post 06-17-2008, 10:38 by cisterni. 0 replies.
Sort Posts: Previous Next
  •  06-17-2008, 10:38 6162

    Msbuild task in F#

    Note: I just found that Robert did a similar thing (in C#) in august using Mono Smile [:)]

    Note: the files for building F# msbuild task are in the attached file.

    F# does not ship (yet!) with support for msbuild, the standard tool used by .NET languages for building systems. It is a MS version of Ant, or a modern Make, or whatever else you prefer to look at it. If you have a look to .csproj files you'll find out that Visual Studio is simply defining the building process as an XML file that can be built on the command line using msbuild. If you download sandcastle, the documentation generation system for .NET languages you'll end up fighting with lots of msbuild files. F#, however, uses its own format in Visual Studio projects and you can use msbuild but you have to use the Execute element to run explicitly the compiler instead of using a more stylish <Fsc .../> tag.

    As an excercise I wrote a so called task for fsc, that is something is used by msbuild to provide the <Fsc .../> tag in msbuild files. And now I'm able to express the building process of my F# applications using msbuild when not using Visual Studio.

    After some code spelunking and heavy use of Reflector I found out that in what can be now considered a traditional interpretation of Xml files, msbuild associates classes with XML elements. Thus the <Csc .../> task that can be found in C# project files is simply a class. The problem has been to figure out how to write such a class, and what interfaces were requested to integrate in the msbuild object model.

    The following is the source code for the Fsc task:

    #light
    open System
    open System.Collections
    open Microsoft.Build.Tasks
    open Microsoft.Build.Framework
    open Microsoft.Win32
    type Fsc() =
    inherit ManagedCompiler() as base
    let AppendWhenNotNull (commandLine:CommandLineBuilderExtension, switchName:string, bag:Hashtable, parameterName:string) =
    if (bag.[parameterName :> obj] <> null) then
    commandLine.AppendSwitch(switchName)
    override x.GenerateFullPathToTool() =
    use k = Registry.LocalMachine.OpenSubKey("Software\\Microsoft\\.NetFramework\\v2.0.50727\\AssemblyFoldersEx\\Microsoft.FSharp")
    (k.GetValue(null) :?> string) + x.ToolName
    override x.ToolName
    with get() = "fsc.exe"

    override x.AddResponseFileCommands (commandLine:CommandLineBuilderExtension) =
    ()


    override x.AddCommandLineCommands (commandLine:CommandLineBuilderExtension) =
    let target = match x.TargetType with
    | "winexe" -> "--target-winexe"
    | "exe" -> "--target-exe"
    | "library" -> "--target-dll"
    | _ -> "--target-exe"

    commandLine.AppendSwitch(target)
    for v in x.References do
    let sw = if v.GetMetadata("CopyLocal") = "true" then "-R " else "-r "
    commandLine.AppendSwitchIfNotNull(sw, v.ToString())
    // FIXME: It does know how to do it?
    //commandLine.AppendSwitchIfNotNull("/addmodule:", this.AddModules, ",")
    AppendWhenNotNull(commandLine, "-g", x.Bag, "EmitDebugInformation")
    // FIXME: Do we have debug type?
    //commandLine.AppendSwitchIfNotNull("/debug:", this.DebugType)

    if (x.Bag.["DelaySign" :> obj] <> null) then
    commandLine.AppendSwitchIfNotNull("--public-keyfile ", x.KeyFile)
    else
    commandLine.AppendSwitchIfNotNull("--keyfile ", x.KeyFile)
    commandLine.AppendSwitchIfNotNull("-O", (x.Bag.["Optimize" :> obj] :?> string))
    commandLine.AppendSwitchIfNotNull("-o ", x.OutputAssembly)
    AppendWhenNotNull(commandLine, "--all-warnings-as-errors", x.Bag, "TreatWarningsAsErrors")

    // FIXME: deal with resources
    // commandLine.AppendSwitchIfNotNull("--link-resource ", base.LinkResources, [| "LogicalName"; "Access" |])
    // commandLine.AppendSwitchIfNotNull("--resource ", base.Resources, [| "LogicalName"; "Access" |])
    commandLine.AppendFileNamesIfNotNull(x.Sources, " ")

    As you can see I've simply derived a class from ManagedCompiler and overridden a number of properties. The code simply mimicks the code of the C# build task and rewrites the input arguments in the correct form accepted by the F# compiler.

    Msbuild requires tasks to be installed in the GAC, thus you have to create a key using sn:

    > sn -k fscbuild.snk

    now you can compile the task as follows:

    > fsc -r Microsoft.Build.Tasks.dll -r Microsoft.Build.Framework.dll -a --keyfile fscbuild.snk -o fsc.build.dll fsc.fs
    > elevate gacutil /i fsc.build.dll

    Note that I use the elevate powertoy on Windows Vista to be able to write in the GAC and cross the UAC barrier.

    Since there is no standard location for finding the F# compiler I followed .NET conventions and read a well defined key within the registry that can be loaded using the following REG file:

    REGEDIT4

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v2.0.50727\AssemblyFoldersEx\Microsoft.FSharp]
    @="c:\\Program Files\\FSharp-1.9.4.10\\bin\\"
    "Description"="FSharp Compiler and Libraries"

    I expect that this will be available in further releases of the language.

    Now you can use the <Fsc .../> task for building the task itself. I followed the lines of C# and introduced the Microsoft.Fsharp.targets file containing common instructions to be included in F# build projects:

     

    <!--

    ***********************************************************************************************

    Microsoft.FSharp.targets

    WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have

    created a backup copy. Incorrect changes to this file will make it

    impossible to load or build your projects from the command-line or the IDE.

    This file defines the steps in the standard build process specific for C# .NET projects.

    For example, it contains the step that actually calls the C# compiler. The remainder

    of the build process is defined in Microsoft.Common.targets, which is imported by

    this file.

    Author Antonio Cisternino

    ***********************************************************************************************

    -->

    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <UsingTask TaskName="Fsc+Fsc" AssemblyName="fsc.build, Version=0.0.0.0, Culture=neutral, PublicKeyToken=bba46fd93395b0e5"/>

    <PropertyGroup>

    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildBinPath)\Microsoft.FSharp.targets</MSBuildAllProjects>

    <DefaultLanguageSourceExtension>.fs</DefaultLanguageSourceExtension>

    <Language>F#</Language>

    </PropertyGroup>

    <PropertyGroup>

    <!-- "None" is not technically a valid DebugType, so we can't pass it in as such

    to the compiler. So here, we modify the properties so they make sense. -->

    <DebugSymbols Condition=" '$(DebugType)' == 'none' ">false</DebugSymbols>

    <DebugType Condition=" '$(DebugType)' == 'none' "></DebugType>

    <_DisabledWarnings>$(NoWarn)</_DisabledWarnings>

    </PropertyGroup>

    <!-- These two compiler warnings are raised when a reference is bound to a different version

    than specified in the assembly reference version number. MSBuild raises the same warning in this case,

    so the compiler warning would be redundant. -->

    <PropertyGroup Condition="('$(TargetFrameworkVersion)' != 'v1.0') and ('$(TargetFrameworkVersion)' != 'v1.1')">

    <_DisabledWarnings Condition="'$(_DisabledWarnings)' != ''">$(_DisabledWarnings);</_DisabledWarnings>

    <_DisabledWarnings>$(_DisabledWarnings)1701;1702</_DisabledWarnings>

    </PropertyGroup>

    <ItemGroup>

    <DocFileItem Include="$(DocumentationFile)" Condition="'$(DocumentationFile)'!=''"/>

    </ItemGroup>

    <PropertyGroup>

    <CoreCompileDependsOn>_ComputeNonExistentFileProperty</CoreCompileDependsOn>

    </PropertyGroup>

    <Target

    Name="CreateManifestResourceNames"

    Condition="'@(ResxWithNoCulture)@(ResxWithCulture)@(NonResxWithNoCulture)@(NonResxWithCulture)'!=''"

    DependsOnTargets="$(CreateManifestResourceNamesDependsOn)"

    >

    </Target>

     

    <Target

    Name="CoreCompile"

    Inputs="$(MSBuildAllProjects);

    @(Compile);

    @(ManifestResourceWithNoCulture);

    $(ApplicationIcon);

    $(AssemblyOriginatorKeyFile);

    @(ManifestNonResxWithNoCultureOnDisk);

    @(ReferencePath);

    @(CompiledLicenseFile);

    @(EmbeddedDocumentation);

    @(CustomAdditionalCompileInputs)"

    Outputs="@(DocFileItem);

    @(IntermediateAssembly);

    $(NonExistentFile);

    @(CustomAdditionalCompileOutputs)"

    DependsOnTargets="$(CoreCompileDependsOn)"

    >

    <Fsc

    AdditionalLibPaths="$(AdditionalLibPaths)"

    AddModules="@(AddModules)"

    CodePage="$(CodePage)"

    DebugType="$(DebugType)"

    DefineConstants="$(DefineConstants)"

    DelaySign="$(DelaySign)"

    EmitDebugInformation="$(DebugSymbols)"

    FileAlignment="$(FileAlignment)"

    KeyContainer="$(KeyContainerName)"

    KeyFile="$(KeyOriginatorFile)"

    MainEntryPoint="$(StartupObject)"

    NoConfig="true"

    NoLogo="$(NoLogo)"

    Optimize="$(Optimize)"

    OutputAssembly="@(IntermediateAssembly)"

    References="@(ReferencePath)"

    Resources="@(ManifestResourceWithNoCulture);@(ManifestNonResxWithNoCultureOnDisk);@(CompiledLicenseFile)"

    ResponseFiles="$(CompilerResponseFile)"

    Sources="@(Compile)"

    TargetType="$(OutputType)"

    ToolPath="$(FscToolPath)"

    TreatWarningsAsErrors="$(TreatWarningsAsErrors)"

    Utf8Output="$(Utf8Output)"

    Win32Icon="$(ApplicationIcon)"

    Win32Resource="$(Win32Resource)"

    />

    </Target>

    <Import Project="$(MSBuildBinPath)\Microsoft.Common.targets" />

    </Project>

    The msbuild file for building the task itself now looks very similar to a C# project (and it is very compact too!):

    <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <PropertyGroup>

    <AssemblyName>fsc.build</AssemblyName>

    <OutputType>Library</OutputType>

    <AssemblyOriginatorKeyFile>fscbuild.snk</AssemblyOriginatorKeyFile>

    <SignAssembly>true</SignAssembly>

    <DelaySign>false</DelaySign>

    <RootNamespace>Fsc</RootNamespace>

    </PropertyGroup>

    <ItemGroup>

    <Reference Include="Microsoft.Build.Tasks" />

    <Reference Include="Microsoft.Build.Framework" />

    <Compile Include="fsc.fs">

    <SubType>Code</SubType>

    </Compile>

    </ItemGroup>

    <Import Project="Microsoft.FSharp.targets" />

    </Project>

     

View as RSS news feed in XML
Powered by Community Server, by Telligent Systems