Skip to content

Using F# or C# script for build definition in Team Build 2012 2013

IlyaAI edited this page Mar 27, 2015 · 1 revision

Intro

A several months ago I faced with necessity to customize build process in Team Build. Microsoft offers visual workflow designer for this purpose. IMHO this is the worst way from all which I can imagine! As for developer it’s much much easier for me to work with some kind of script and I started looking for such alternative.

FAKE (F# Make) looked very promising. Unfortunately FAKE didn’t provide TFS integration out of box. I tried to extend it and encountered some conceptual problems (you could read more details here). But idea itself was great and I decided to write my own implementation – AnFake (Another F# Make).

Using AnFake

First we should set-up AnFake.

Then we could define build steps using F# script (actually, C# also supported but has a little more verbose syntax). Below the trivial example which compiles and runs unit-tests:

let out = ~~".out"               // ~~ operator converts string to FileSystemPath object
let productOut = out / "product" // FileSystemPath objects can be combined with operator /
let testsOut = out / "tests"
let tests = !!"*/*.Test.csproj"  // !! operator creates FileSet object from wildcarded path
let product = 
    !!"*/*.csproj"
    - tests                      // - operator excludes files from FileSet

// defining target 'Compile'
"Compile" => (fun _ ->
    MsBuild.BuildRelease(product, productOut) // run MSBuild for product projects
    MsBuild.BuildRelease(tests, testsOut)     // run MSBuild for test projects
)

"Test" => (fun _ -> 
    // run VSTest.Console on all *.Test.dll files from testsOut folder
    VsTest.Run(testsOut % "*.Test.dll")
)

"Build" <== ["Compile"; "Test"]     // 'Build' consists of 'Compile' and 'Test'

A bit more complex example which looks up a last good build, takes its output, runs performance test, puts result into database, calculate statistical threshold over last 5 measurements and checks new result against this threshold:

let out = ~~".out"
let binOut = out / "bin"

// declaring data contract for performance report
[<DataContract>]
type PerformanceReport () =
    [<DataMember>] member val ChangesetId: int = 0 with get, set
    [<DataMember>] member val ElapsedTime: double = 0.0 with get, set
    [<DataMember>] member val BytesProcessed: int64 = 0L with get, set

"Test.Performance" => (fun _ ->
    let buildDefName = MyBuild.GetProp("productBuild")  // gets externally passed parameter
    // looks up last build with quality 'Unit-Tests Passed' in Team Build
    let goodBuild = TfsBuild.QueryByQuality(buildDefName, "Unit-Tests Passed", 1).First()
    
    // copying goodBuild's output to local folder
    Files.Copy(goodBuild.GetDropLocationOf(ArtifactType.Deliverables) % "*", binOut)
    
    // building command line arguments for performance meter tool
    let reportPath = out / "PerfMeter.report"
    let args = 
        (new Args("--", " "))            
            .Option("threads", 4)
            .Option("report", reportPath)
            .ToString()

    // running performance meter tool
    Process.Run(fun p -> 
        p.FileName <- binOut / "PerfMeter.exe"
        p.Arguments <- args        
    ).FailIfExitCodeNonZero("PerfMeter.exe FAILED.") 
    |> ignore

    // loading performance report
    let report = Json.ReadAs<PerformanceReport>(reportPath.AsFile())
    report.ChangesetId <- VersionControl.CurrentChangesetId
    
    Nh.MapClass<PerformanceReport>()
    Nh.DoWork(fun uow ->
        // querying previous 5 measurements from DB
        let prevReports = 
            uow.Query("from PerformanceReport order by id desc")
                .SetMaxResults(5)
                .List<PerformanceReport>()
        
        // saving new result
        uow.Save(report)
        uow.Commit()

        // calculating and checking threshold
        if prevReports.Count = 5 then        
            let prevSpeeds = prevReports.Select(fun rep -> (double)rep.BytesProcessed / rep.ElapsedTime)
            let avg = prevSpeeds.Average()
            let sig2 = prevSpeeds.Average(fun x -> (x - avg)*(x - avg))

            let speed = (double)report.BytesProcessed / report.ElapsedTime
            let threshold = avg - Math.Sqrt(sig2);
            if speed < threshold then
                MyBuild.Failed(
                    "The last reported speed {0:F2} KB/s is under threshold {1:F2} KB/s.", 
                    speed / 1024.0, threshold / 1024.0
                )
    )
)

Of course, AnFake is built by itself – take a look on real-life example of build.fsx or build.csx

To run ‘build.fsx’ in Team Build AnFake provides special build process template ‘AnFakeTemplate.xaml’ which downloads sources from version control then invokes AnFake.exe which runs script. Below the simple example of build summary report in Team Build:

AnFake Team Build Run

With AnFake all customization are made in script file tracked by version control which makes build definitions much more maintainable.

Right now we are actively using AnFake in our build environment and I think it might be useful for other teams who work with TFS. If you’ve read to the end, please spend one minute more and answer just a couple of questions. This helps me to estimate interest to such tool.

Thank you.