This article is also on Youscribe

What’s FAKE?

Fake is a DSL permitting to write advanced automated builds in FSharp. It can be used by beginners in FSharp because the DSL can hide some FSharp code comlexity. We can define multiple targets like in a Makefile.

For example:

let buildDir = "./build/"

Target "Clean" (fun _ ->
    trace "cleaning ..."
	CleanDir buildDir
)

Target "Packages" (fun _ ->
    trace "Restoring nugets"
    RestorePackages()
)

Target "Compile" (fun _ ->
    MSBuildRelease androidBuildDir "Build" !!"src/**/*.csproj"
        |> Log "AppBuild-Output: "
	// We use MSBuild and XBuild for .net projects
	// but we can extend the DSL to use everything else
)

Target "Tests" (fun _ ->
	// we can run the unit tests ...
)

Target "Install" (fun _ ->
	// if we use an integration server, we can deploy the project
	// but we can also install an app like a classic "make install"

	buildDir
	|> directoryInfo
    |> filesInDir
    |> Seq.map (fun f -> f.FullName)
    |> CopyFiles ProgramFilesX86
)

// define the target ordering and dependencies
"Clean"
   ==> "Packages"
   ==> "Compile"
   ==> "Tests"
   ==> "Install"

// if no target is specified in args, we run "Install"
RunTargetOrDefault "Install"

Build and publish a simple APK

Fake contains the XamarinHelper module helping developers to script their APK building.

There is also the AndroidPublisher module permitting to upload the APK on the Google Play Store.

In the following sample, we can build Apk with the default target and publish them with the “Publish” target.

Usage:

 Fake.exe build.fsx "target=publish"

Script:

#r "packages/FAKE/tools/FakeLib.dll"

let androidBuildDir = "./build/"
let androidProdDir = "./pack/"

androidProdDir |> ensureDirectory

//Clean old apk
Target "Clean" (fun _ ->
    CleanDir androidBuildDir
    CleanDir androidProdDir
)

Target "Android-Package" (fun () ->
    AndroidPackage(fun defaults ->
                    { defaults with
                        ProjectPath = "Path to my project Droid.csproj"
                        Configuration = "Release"
                        OutputPath = androidBuildDir
                        Properties = ["MSBuild property", "MSBuild property value"]
                    })

    |> AndroidSignAndAlign (fun defaults ->
        { defaults with
            KeystorePath = @"path to my file.keystore"
            KeystorePassword = "my password"
            KeystoreAlias = "my key alias"
        })
    |> fun file -> file.CopyTo(Path.Combine(androidProdDir, file.Name)) |> ignore

)

Target "Publish" (fun _ ->
    // I like verbose script
    trace "publishing Android App"
    let apk = androidProdDir
                    |> directoryInfo
                    |> filesInDir
                    |> Seq.filter(fun f -> f.Name.EndsWith(".apk"))
                    |> Seq.exactlyOne
    let apkPath = apk.FullName
    tracefn "Apk found: %s" apkPath
    let mail = "my service account mail@developer.gserviceaccount.com"
    // Path to the certificate file probably
	// named 'Google Play Android Developer-xxxxxxxxxxxx.p12'
    let certificate = new X509Certificate2
                                (
                                    @"Google Play Android Developer-xxxxxxxxxxxx.p12",
                                    "notasecret",
                                    X509KeyStorageFlags.Exportable
                                )
    let packageName = "my Android package name"

    // to publish an alpha version:
    PublishApk
        { AlphaSettings with
            Config =
                {
                    Certificate = certificate;
                    PackageName = packageName;
                    AccountId = mail;
                    Apk = apkPath;
                }
        }

)

Target "Android-Build" (fun _ ->
    !! "**/my project Droid.csproj"
        |> MSBuildRelease androidBuildDir "Build"
        |> Log "BuildAndroidLib-Output: "
)

Target "Default" (fun _ ->
    trace "Building default target"
    RestorePackages()
)

"Clean"
    ==> "Android-Package"
    ==> "Default"

RunTargetOrDefault "Default"

Build one APK per ABI

I modified XamarinHelper to give the possibility to specify ABI target when using AndroidPackage.

As described in Xamarin docs multiple APK support is good for reduce the size of the APK and support different CPU architectures.

I’m not fan of excessively complex version code generation. I published some applications using a simpliest method. I just increment of 1 each ABI version code.

Google imposes only one constraint:

We must respect the order x86 < x86_64 < ArmEabi < ArmEabiV7a < Arm64V8a.

For example, if your debug version code is 5, your APKS ‘s version codes should be:

  • 5 for MyApp-X86.apk
  • 6 for MyApp-X86_64.apk
  • 7 for MyApp-armeabi.apk
  • 8 for MyApp- armeabi-v7.apk
  • 9 for MyApp- armeabiv64-v8a.apk

The next built script example is use for my sokoban fsharp implementation

//#r "packages/FAKE/tools/FakeLib.dll"
#I "/Users/rflechner/Documents/development/FAKE/build"
#r "FakeLib.dll"

open System
open System.IO
open Fake
open XamarinHelper
open AndroidPublisher

let androidBuildDir = "./build/"
let androidProdDir = "./pack/"

androidProdDir |> ensureDirectory

Target "Clean" (fun _ ->
    CleanDir androidBuildDir
    CleanDir androidProdDir
)

Target "Android-Package" (fun () ->
    AndroidPackage(fun defaults ->
                    { defaults with
                        ProjectPath = "Sokoban.Droid/Sokoban.Droid.fsproj"
                        Configuration = "Release"
                        OutputPath = androidBuildDir
                    })
    |> fun file -> file.CopyTo(Path.Combine(androidProdDir, file.Name)) |> ignore
)

Target "Android-MultiPackages" (fun () ->
    let versionStepper = (fun v t -> match t with
                                     | AndroidAbiTarget.X86 c -> v + 1
                                     | AndroidAbiTarget.X86And64 c -> v + 2
                                     | AndroidAbiTarget.ArmEabi c -> v + 3
                                     | AndroidAbiTarget.ArmEabiV7a c -> v + 4
                                     | AndroidAbiTarget.Arm64V8a c -> v + 5
                                     | _ -> v)
    let abis = AndroidPackageAbiParam.SpecificAbis
                ([ AndroidAbiTarget.X86({ SuffixAndExtension="-x86.apk"; })
                   AndroidAbiTarget.ArmEabi({ SuffixAndExtension="-armeabi.apk"; })
                   AndroidAbiTarget.ArmEabiV7a({ SuffixAndExtension="-armeabi-v7a.apk"; })
                   AndroidAbiTarget.X86And64({ SuffixAndExtension="-x86_64.apk"; })
                ])
    let files = AndroidBuildPackages(fun defaults ->
                            { defaults with
                                ProjectPath = "Sokoban.Droid/Sokoban.Droid.fsproj"
                                Configuration = "Release"
                                OutputPath = androidBuildDir
                                PackageAbiTargets = abis
                                VersionStepper = Some(versionStepper)
                            })

    for f in files do
        printfn "- apk: %s" f.Name

    files
    |> Seq.iter (
		fun file ->
			file.CopyTo(Path.Combine(androidProdDir, file.Name))
			|> ignore
		)
)

Target "Android-Build" (fun _ ->
    !! "**/Sokoban.Droid.fsproj"
        |> MSBuildRelease androidBuildDir "Build"
        |> Log "BuildAndroidLib-Output: "
)

Target "Default" (fun _ ->
    trace "Building default target"
    RestorePackages()
)

"Clean"
    ==> "Android-Package"
    ==> "Default"

RunTargetOrDefault "Default"

Usage:

 Fake.exe build.fsx "target=Android-Package"

OR:

 Fake.exe build.fsx "target=Android-MultiPackages"