Skip to content

Instantly share code, notes, and snippets.

@mavnn
Last active November 28, 2016 15:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mavnn/4944580 to your computer and use it in GitHub Desktop.
Save mavnn/4944580 to your computer and use it in GitHub Desktop.
Literate build.fsx
(**
## References
As they aren't part of a project, fsx files need
to reference all of their dependencies within the
file.
You'll always want to reference FakeLib. VersionUpdater
is an inhouse tool we use for handling feature branch
support within TeamCity.
*)
#r @"tools\FAKE\tools\FakeLib.dll"
#r @"tools\15below.VersionUpdater.dll"
#r "System.Xml.Linq"
open Fake
open System
open System.IO
open System.Text.RegularExpressions
open System.Xml.Linq
open FifteenBelow.VersionUpdater
(***
## Set up
A set of environment variables we'd expect to be supplied
by the build server if we're on TeamCity. If not, default
them.
*)
let configuration = "Release"
let core_version_number = environVarOrDefault "core_version_number" "0.0.0"
let build_number = environVarOrDefault "build_number" "0"
let teamcity_build_branch = environVarOrDefault "teamcity_build_branch" (sprintf "%s/release" core_version_number)
let vcsroot_branch = environVarOrDefault "vcsroot_branch" "development"
let nugetPath = currentDirectory @@ "tools" @@ "nuget" @@ "NuGet.exe"
(***
A helper method which sets up the build parameters we use on most of our projects.
The only real thing of note here is the `FscTools` property that will allow us
to use the version of the F# compiler in the repository.
You might want to change any or all of the rest depending on taste: we've
gone for very minimal output during the build, but writing both errors
and warnings to file for future reference if needed.
As you can see, things like the Verbosity of the build are strongly typed
in FAKE, removing a lot of the need to check the underlying tools manual
every two lines of code.
FAKE in general makes heavy use of records to set parameters for tools.
This allows for a default set of parameters to be provided, and then you
can override only the values you need to change with the `{ record with ... }`
syntax.
*)
let buildSolution sln targets =
let setParams defaults =
{ defaults with
Verbosity = Some(Quiet)
Targets = [targets]
Properties = [
"DeployTarget","Package"
"DeployOnBuild","True"
"CreatePackageOnPublish","True"
"Optimize", "True"
"DebugSymbols", "True"
"Platform", "Any CPU"
"Configuration",configuration
"FscTools", currentDirectory @@ "tools" @@ "FSharp\\"]
FileLoggers = Some
[{ Number = 8
Filename = Some "errors.log"
Verbosity = Some Normal
Parameters = Some [ErrorsOnly]
};
{
Number = 9
Filename = Some "warnings.log"
Verbosity = Some Normal
Parameters = Some [WarningsOnly;Append]
}]
}
build setParams sln
(***
## Targets
Our first `Target`! Targets in FAKE are like build targets in most
build systems. They allow us to group functionality into steps that
we can then define dependencies between.
In FAKE we create the targets first and then set up the dependency
tree at the end.
This first target uses our inhouse VersionUpdater to update any
AssemblyInfo files with the current version from TeamCity
(including semantic versions for feature branches). If this build
was creating nuget packages, we'd also update the nuspec files
here with relevant version information.
It then reports back to TeamCity what it has set these values to.
*)
Target "UpdateVersions" (fun _ ->
let assemblyInfos = !! (sprintf "%s/**/AssemblyInfo.*" currentDirectory)
|> Seq.map (fun name -> new FileInfo(name))
let returns = Update.Do(teamcity_build_branch, vcsroot_branch, core_version_number, build_number, true, assemblyInfos)
setEnvironVar "AssemblyVersion" returns.AssemblyVersion
setEnvironVar "NugetVersion" returns.NugetVersion
setEnvironVar "SafeBranchName" returns.SafeBranchName
SetTeamCityParameter "safe_branch" returns.SafeBranchName
SetTeamCityParameter "nuget_version" returns.NugetVersion
)
(**
By default, Visual Studio 2012 creates project files that try and use
the version of Microsoft.FSharp.targets that was installed with it. Makes
sense, and we don't want to change them as this breaks tools like NCrunch.
Unfortunately, this version of the targets does not support the `FscTools`
property correctly. So when we're building with FAKE we first mangle the
fsproj files to use the .targets file from within the repository.
At the end of the Target we use the "ActivateFinalTarget" function to
make sure that these fsproj files will be restored, even if another later
build step fails.
*)
Target "UpdateFSharpTargets" (fun _ ->
let targetFullPath = FileInfo(currentDirectory @@ "tools" @@ "FSharp" @@ "Microsoft.FSharp.targets").FullName
trace ("FullPath: " + targetFullPath)
let alterProj (projXml : XDocument) =
let name = XNamespace.Get "http://schemas.microsoft.com/developer/msbuild/2003"
projXml.Element(name + "Project").Elements(name + "Import")
|> Seq.toList
|> List.iter (fun elem -> elem.Remove())
let importOverride =
new XElement(name + "Import")
importOverride.Add(XAttribute(XName.Get "Project", targetFullPath))
projXml.Element(name + "Project").Add(importOverride)
projXml
!! (sprintf "%s/**/*.fsproj" currentDirectory)
|> Seq.map (fun file -> FileInfo(file).CopyTo(file + ".orig", true) |> ignore; file)
|> Seq.map (fun file -> XDocument.Load(file), file)
|> Seq.map (fun (xDoc, file) -> alterProj xDoc, file)
|> Seq.iter (fun (xDoc, file) -> xDoc.Save(file))
ActivateFinalTarget "RestoreFSharpTargets"
)
(**
This is the implementation of the final target mentioned above. Final targets always get
run, even in the event of a build failure. Normal targets are aborted if a previous target
has thrown an exception.
*)
FinalTarget "RestoreFSharpTargets" (fun _ ->
!! (sprintf "%s/**/*.fsproj.orig" currentDirectory)
|> Seq.iter (fun file -> FileInfo(file).CopyTo(file.[0..(String.length file - 5)], true) |> ignore)
)
(**
This target simply runs nuget.exe to restore all nuget references within the repository.
There is now a built in function in FAKE called RestorePackages that does the same thing,
but we'd written this already!
*)
Target "InstallPackages" (fun _ ->
let updatePackages packagesConfig =
let timeOut = TimeSpan.FromMinutes 5.
let args = sprintf @"install ""%s"" -OutputDirectory ""packages""" packagesConfig
let result = ExecProcess (fun info ->
info.FileName <- nugetPath
info.WorkingDirectory <- currentDirectory
info.Arguments <- args) timeOut
if result <> 0 then failwithf "Error during Nuget update. %s %s" nugetPath args
!! @"**\packages.config"
|> Seq.iter updatePackages
)
(**
As above but updates rather than restoring packages.
This is never called as part of the build process, but
can be run as a target in it's own right if you want to
update all nuget packages in the repository.
*)
Target "UpdatePackages" (fun _ ->
let updatePackages packagesConfig =
let timeOut = TimeSpan.FromMinutes 5.
let args = sprintf @"update ""%s""" packagesConfig
let result = ExecProcess (fun info ->
info.FileName <- nugetPath
info.WorkingDirectory <- currentDirectory
info.Arguments <- args) timeOut
if result <> 0 then failwithf "Error during Nuget update. %s %s" nugetPath args
!! @"**\packages.config"
|> Seq.iter updatePackages
)
(**
This target searches for and then runs NUnit test dlls.
NUnit.Runners has been set up as a nuget dependency and
we use one of FAKE's NuGet helper methods to get the
current version of NUnit.Runners installed to determine
it's location.
After that, we use the NUnit helper method to actually
run the tests. This includes functionality to inform
TeamCity where the test results are, so we do not need
to configure that within the TeamCity settings.
If (and only if) all of the tests pass, we activate the
PushPackages target, but only if we are building in CI.
*)
Target "RunTests" (fun _ ->
let nunitVersion = GetPackageVersion "packages" "NUnit.Runners"
let nunitPath = sprintf @"packages/NUnit.Runners.%s/tools/" nunitVersion
!! (sprintf @"*.Tests\**\bin\%s\*.Tests.dll" configuration)
|> NUnit (fun p -> { p with ToolPath = nunitPath; DisableShadowCopy = true; OutputFile = @"TestResults.xml"; Framework = "4.0" })
if not isLocalBuild then
ActivateFinalTarget "PushPackages"
)
(**
These are the two projects that we will want the build results packaged
from when a build is carried out on the build server.
Another FAKE helper method allows us to quickly and easily build the
zip files we want to publish as build artifacts, and the `PublishArticfact` (sic)
method then notifies TeamCity where the files are.
*)
FinalTarget "PushPackages" (fun _ ->
let ProjectNames =
[
"BuildOctopusProject"
"ProcessCustomerQuestionaire"
]
ProjectNames
|> List.map (fun proj ->
let zipFile = currentDirectory @@ proj + ".zip"
let projDir = currentDirectory @@ proj @@ "bin" @@ configuration
Zip projDir zipFile <| Directory.EnumerateFiles(projDir)
zipFile)
|> List.iter (fun proj -> PublishArticfact proj)
)
(**
A simple Target that just calls our buildSolution helper from the top of the
file with a "Clean" target.
*)
Target "Clean" (fun _ ->
buildSolution "PackagedProductTools.sln" "Clean"
)
(**
And, at last, the target that actually builds the solution.
Before it starts, it deletes the "warnings.log" from previous
builds. In case you're wondering why we're appending to the log
file, it's because that way we could build multiple solutions
in this target and aggregate the warnings from all of them.
*)
Target "Build" (fun _ ->
DeleteFile "warnings.log"
buildSolution "PackagedProductTools.sln" "Build"
)
(**
To finish things off, we have a no-op "Default" target,
that we will use for defining a dependency tree to give
the default behaviour of the script if it's run without
a named target.
*)
Target "Default" DoNothing
(**
FAKE has a few custom operators defined or creating dependency trees.
They operate on strings, which are the target names defined above.
The `==>` operator implies that the target on the left is a requirement
for the target on the right.
The `<=>` operator implies that the two targets on each side do not depend
on each other, but they share the same dependency chain up to that point.
The `=?>` operator (not used below) allows you to set up conditional dependencies
based on a predicate. As the predicate is just a standard F# statement
it can use any information available (build target requested, whether the build
is on a build server, etc) to decide if a particular dependency exists for
this build.
So here, we're saying that both clean and build rely on "UpdateFSharpTargets".
We're then going on to say that:
* Default depends on "RunTests"
* ...that depends on "Build"
* ...etc
*)
"UpdateFSharpTargets"
==> "Clean" <=> "Build"
"InstallPackages"
==> "Clean" <=> "UpdateVersions"
==> "Build"
==> "RunTests"
==> "Default"
(**
Having set everything up above, it's time to actually start the build process
going. This little piece of code means that you can either run the build file
with FAKE.exe and no arguments:
`.\tools\FAKE\FAKE.exe build.fsx`
or with a target specified:
`.\tools\FAKE\FAKE.exe build.fsx target="Build"`
(Which given our dependency tree would build the solution but not run the tests.)
*)
// start build
RunParameterTargetOrDefault "target" "Default"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment