Skip to content
Oleg edited this page May 18, 2017 · 3 revisions

Recipe computation

Recipe body is computation expression of type recipe. This computation returns Recipe type and is very similar to async computation. Both regular code (such as assignments/binding, loops and conditional expressions) and do! notation can be used in recipe* body.

Defining recipe

Defining recipe is similar to defining a regular function:

let downloadFile name = recipe {
    let! fileData = WebClient.DownloadStringAsync name
    return fileData.Length
}

Call another task/recipe

recipe {
    let! result = downloadFile "main.cpp"
    ...
}

Notice let-bang operator must be used for calling recipe or async functions. If recipe does not return a value do! operator should be used.

Access execution context

Execution options and settings could only be accessed from within recipe computation:

recipe {
    // HOWTO get the target name of currently executing rule
    let! name = getTargetFile()
    let filename = name.FullName
    // which has a shortcut:
    let filename = getTargetFullName()

    // HOWTO get execution options
    let! opts = getCtxOptions()
    let rootPath = opts.ProjectRoot
    ...
}

Getting script and environment variables

"verinfo.txt" ..> recipe {
    let! envver = getEnv "VER"
    let ver = envver |> function | Some x -> x | _ -> "v0.1"
    do! writeText <| sprintf "$version: %s" ver

    let! isDebug = getVar "DEBUG" |> Recipe.map (function | Some x -> x | _ -> "0")
}

Recipe.map is internal function that allow to map a result value of recipe.

Request the other artifact to be built

In case one artifact depends on the other, for example application executable is statically linked to utility assembly, you usually request xake to build dependency by calling builltin need recipe:

"app.exe" ..> recipe {
    do! need ["ver.cs"; "somelib.dll"]
    ...

Recipe will put the requested targets to the build queue (unless they are not built already) and will pause execution until all artifacts are ready.

It will also record that currently executing target depends on requested files and will skip the build next time in case dependencies were not modified.

Inner tasks such as csc, fsc, copyFile request all source files and libraries automatically.

Execute async function

Recipe computation allow async functions to be called:

recipe {
    let! result = downLoadFileAsync "http://nowhere.com/file.zip"
    ...

Writing to log

Internal trace function writes the message to the log. The message is written to all log targets (such as console and file), messages are prefixed with a message level and identifier (index) of currently executing task for tracing the source of message.

do! trace Message "Hello world"
do! trace Info "Hello, %s!" "username"

Level parameter defines the class/severity of the message. Messages are filtered based on level. level accepts one of the following values: | Message | Error | Command | Warning | Info | Debug | Verbose | Never

When to define recipe or function

Define a recipe in case any if the following conditions met:

  • you need an access the execution context
  • asynchronous functions are used
  • calling xake functions for tracing, demanding other artifacts etc

Use functions in other cases.

Control flow

if, for and while statements are supported:

recipe {
    if s1 = "2" then
        do something()
    /// else block is also allowed after if

    for i in [1..3] do
        do! trace Info "%A" i
 
    let j = ref 3
    while !j < 5 do
        do something (sprintf "j=%i" !j)
        j := !j + 1
}

Exception handling: try/with/finally

recipe computation expression supports try/with and try/finally blocks.

// create a helper task/recipe
let log = trace Message
...
recipe {
  do! log "before try"

  try
    try
        do! log "Body executed"
        failwith "Ouch"
    with e ->
        do! log "Exception: %s" e.Message
  finally
    printfn "Error!"
}

tasks (with do! notation) are allowed in with block but aren't in finally block. This is limitation of F#'s computation expressions.

WhenError function

Intercepts errors (exceptions) and allows to define a custom handler.

  phony "main" (recipe{
    do! trace Message "The exception thrown below will be silently ignored"
    failwith "some error"
    } |> WhenError ignore)

FailWhen

Raises the exception if action's result meet specified condition. E.g. the following code raises error in case errorlevel (result of shell command execution) gets non-zero value.

do! shell {cmd "dir"} |> FailWhen ((<>) 0) "Failed to list files in folder"
// or just
do! shell {cmd "dir"} |> CheckErrorLevel
Clone this wiki locally