Build a Xamarin Android application with Fake
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"