TheSharperDev

Posts about C# and F#

Using the FSharp.Compiler.Service to Compile a fsproj

I’m currently building a compiler/transcompiler for F# to WebAssembly called tinyfs

Initially TinyFS was only building individual F# files. But my goal was to support building an entire *.fsproj file.

There does exist some documentation on that, but mentions .NETFramework so not very recent.

In slack I asked a question about it and was able to get it answered. Since Slack isn’t web accessible and only shows the last 90 days worth of info, I decided to document it here for posterity sake.

Slack Conversation

My Initial Question

What’s the canonical way of having the FSharp.Compiler.Service build an fsproj and having it return the AST? I’ve been able to parse and type check individual snippets of code by writing to a script file (.fsx):

let parseAndCheckSingleFile (checker: FSharpChecker) (input: string) =
    let file = Path.ChangeExtension(System.IO.Path.GetTempFileName(), ".fsx")
    File.WriteAllText(file, input)
    // Get context representing a stand-alone (script) file
    let projOptions, _errors =
        checker.GetProjectOptionsFromScript(file, SourceText.ofString input, assumeDotNetFramework = false)
        |> Async.RunSynchronously

    checker.ParseAndCheckProject(projOptions)
    |> Async.RunSynchronously

I imagined I could pass FSharpChecker an *.fsproj file and the FSharp.Compiler.Service could determines the files to compile based on the *fs.proj file. But I’ve been having to pass the individual files for it to compile correctly:

let parseAndCheckProject (checker: FSharpChecker) (projectPath: string) (projectFilePath: string) =
    let args: string array =
        [|
           yield $"{projectPath}/Library.fs"
           |]
    let projectOptions = 
        checker.GetProjectOptionsFromCommandLineArgs(projectFilePath, args) // projectFilePath args // GetProjectOptionsFromProjectFile(projectFilePath)
        
    // Parse and check the entire project
    let projectResults = 
        checker.ParseAndCheckProject(projectOptions)
        |> Async.RunSynchronously
        
    projectResults

Is this the correct way to do this? Or am I missing something?

I’ve been using this doc as a basis for the code above: https://fsharp.github.io/fsharp-compiler-docs/fcs/project.html

As my *.fsproj project grows (or I use other ones), I’d rather not have to specify every file I want it to compile. Especially since the *.fsproj already has that information.

Chet Husk’s (chethusk) Reply

You need to ‘crack’ the project file (which means doing MSBuild evaluation and pulling command line args from it) and then convert that MSBuild driven information into an FSharpProjectOptions. I maintain the ionide/proj-info library to do this in process, but I know Fable has at least one other version of this process (edited)

Me

So for a project that has 100 fs files in it, there’s 100 individual fs files passed via the command line args in order to build the project?

Chet

Yes

Same is true for C#. Also the command line contains every dll in the .net base class library. It’s a lot of args!

Me

Thank you for clarifying, learned something new today.

Conclusion

This helped point me in the right direction, I hope it can help someone else as well.