This post outlines a real-world (anonymized) example of how to use FAKE to create a build script that gets the latest source code from Team Foundation Server (TFS), builds the SharePoint 2013 provider-hosted ASP.NET MVC/API app, and then takes it for a quick spin.
In terms of Visual Studio solution structure, the provider-hosted app is created using the build-in project template. It results in the creation of two projects: one that holds the declarative logic (mostly XML, no .NET code) of the app to be deployed to SharePoint and one that's a regular ASP.NET MVC/WebAPI app to be hosted outside SharePoint. Deploying the declarative app to SharePoint configures SharePoint to seamlessly integrate with the MVC/WebAPI app.
In order to build the solution consisting of the two projects, we need to go through the following steps:
Before getting to the actual build steps, called targets in FAKE, we must first load relevant FAKE assemblies, added to the solution through Nuget, and define various settings required by the targets. Defining these settings at the top of the script makes each target relatively independant of actual values and avoids having too many magic string and numbers around.
#r "packages/FAKE.2.15.4/tools/FakeLib.dll" #r "packages/FAKE.2.15.4/tools/Fake.IIS.dll" open System.Net open Fake open Fake.IISExpress // common settings let baseDir = "c:/users/ronnie/desktop/AcmeApp-CI" let relativeSourceDir = "AcmeRoot/DevBranch/AcmeApp" let absoluteSourceDir = baseDir @@ relativeSourceDir let workspaceName = "AcmeApp-CI" let projectCollection = "AcmeCorp.visualstudio.com\DefaultCollection" let mvcAppPort = 8080 // external tools let tf = "C:/Program Files (x86)/Microsoft Visual Studio 12.0/Common7/IDE/tf.exe"
To minimize the risk of any previous build affecting the current one, producing false positives or false negatives, we disconnect the source control folder from its associated TFS workspace and delete both. This step might fail on first build or if a previous build failed to complete the target. In either case, we don't want the entire build to fail and thus probe and react to the tf.exe exit code.
Target "DeleteWorkspace" <| fun _ -> let args = sprintf "workspace /delete /noprompt %s" workspaceName let exitCode = Shell.Exec(tf, args) // exitCode 100 means "The workspace does not exist" if not (exitCode = 0 || exitCode = 100) then failwithf "Unable to delete workspace: %i" exitCode FileHelper.DeleteDir(baseDir)
Next we (re)create the source control folder and associate it with a TFS workspace. For tf.exe to properly setup this relationship, it's important we set the current directory of tf.exe to the recently created source control folder. As a side-effect tf.exe creates a special $tf folder for storing metadata within it's current directory.
Target "Createworkspace" <| fun _ -> FileHelper.CreateDir(baseDir) let args = sprintf "workspace /new /noprompt %s /collection:%s" workspaceName projectCollection let exitCode = Shell.Exec(tf, args, baseDir) if exitCode <> 0 then failwithf "Unable to create workspace: %i" exitCode
From inside the TFS project collection, we want to recursively retrieve the part of the directory structure that maps to our source code. This can potentially be a long path that includes both solution, project, and branch folders:
Target "GetLatest" <| fun _ -> let args = sprintf "get \"$/%s\" /recursive" relativeSourceDir let exitCode = Shell.Exec(tf, args, baseDir) if exitCode <> 0 then failwithf "Unable to get latest: %i" exitCode
Compiling the SharePoint and the MVC/WebAPI apps is likely the easiest part of the entire build process. It's simply a matter of delegating to MSBuild. One thing to note though is that in addition to the typical MSBuild build target, SharePoint app projects have a special "Package" target that created the package to deploy to SharePoint. To create this package the SharePoint app project must first be compiled, which has a dependency on the MVC/WebAPI app, compiling it as well.
Target "BuildApp" <| fun _ -> // because of build dependencies this'll build both SharePoint and MVC/WebAPI projects MSBuildDebug "" "Package" [absoluteSourceDir @@ "AcmeAppMVC/AcmeApp.csproj"] |> ignore
As a quick test, we want to make sure the web server can actually run our MVC/WebAPI app. We could deploy the app to IIS but it works just as well with IISExpress. All we need to do is point IISExpress to the MVC/WebAPI folder and tell it which port to use. Before starting IISExpress, however, we modify web.config to point to a LocalDB instead of a MSSQL Server database.
Target "Deploy" <| fun _ -> // iisexpress requires the passed in path to be normalizes to distinguish its arguments let mvc = normalizeFileName (absoluteSourceDir @@ "AcmeAppMVC") // update web.config connection string used by Entity Framework updateConnectionString "AcmeAppDatabase" @"Server=(localdb)\v11.0;AttachDBFilename=|DataDirectory|\AcmeApp.mdf;Integrated Security=true;" (mvc @@ "web.config") // iisexpres is automatically killed by FAKE when the script terminates ProcessHelper.StartProcess(fun psi -> psi.FileName <- IISExpressDefaults.ToolPath psi.Arguments <- sprintf "\"/path:%s\" /port:%i" mvc mvcAppPort) // database creation with EF requires data directory to exists beforehand FileHelper.CreateDir(mvc @@ "App_Data") // iisexpress starts asynchronously which on rare occasions seems to lead to // the server not being fully operational when it receives the request, // resulting in an Internal Server Error exception. let triggerDatabaseCreation = sprintf "http://localhost:%i/home/CreateDatabase" mvcAppPort (new WebClient()).DownloadString(triggerDatabaseCreation) |> ignore
Lastly, we connect the targets together in a dependency chain. Every target ultimately depends on DeleteWorkspace to execute first and no target depends on Deploy. In other words, if we ask FAKE to execute the Deploy target, it'll trigger the execution of all the other targets starting with DeleteWorkspace.
Target "Default" DoNothing "DeleteWorkspace" ==> "CreateWorkspace" ==> "GetLatest" ==> "BuildApp" ==> "Deploy" RunTargetOrDefault "Default"