Volume 31 Number 10
Transform Source Code to Deployable Artifacts with TFBuild
By Kraig Brockschmidt | October 2016
In my last article, “The Source of Truth: The Role of Repositories in DevOps”(bit.ly/2bKeC2T), I discussed the vital role that source control plays in the overall release pipeline, shown in Figure 1. I’ve described the release pipeline as the collection of processes that transform code in the source repository into customer-ready apps and services, and delivers those to customer devices and customer-accessible servers. The release pipeline, too, is simply a list of the steps that are necessary to make a release happen, irrespective of automation. The practice of DevOps thus begins with knowing what steps and processes are involved in a release, after which you can then incrementally automate those processes to lower costs and increase quality.
Figure 1 Build Transforms Source Code into the Artifacts Needed in the Rest of the Release Pipeline
Think of Build as you would about physical construction. A large pile of building materials, such as lumber, concrete and rebar, nails, screws, windows, pipe, wire, roofing, insulation, fixtures, and so forth, contains the potential for a house, but the pile will not automatically turn itself into such a structure. That happens only by having someone apply the knowledge of how all those pieces get put together, step by step. That’s what “doing a build” is all about. In the realm of mobile apps, especially, it might even be said that there are multiple piles of source materials, some of which are shared among target platforms, and others of which are unique.
Of course, a big advantage with software is that doing a build doesn’t consume the source materials in the process. You can do a build as often as you want, as many times as you want and, if the process is automated, at very little cost. Continuous integration relies on these characteristics. You can build multiple, independent artifacts from the same source code, each of which has a separate release pipeline. This is often the case with mobile apps. In the MyDriving project (aka.ms/iotsampleapp), the example I’ve been referring to in this series, there are separate builds for iOS, Android and Windows, and for the code that’s deployed to Azure App Service, as illustrated in Figure 2. (Note that you can use Build/CI for ongoing testing for your dev team without necessarily feeding into a release pipeline.)
Figure 2 The MyDriving Project Has Four Builds and Four Release Pipelines
Know, too, that Visual Studio Team Services can draw from repositories outside of a Team Project, allowing you to manage certain libraries for your app in public, open source repositories, while the proprietary portions are kept private. Any given build definition can draw from only a single repository, but a Team Project can employ any number of build definitions.
The Role of Build in DevOps
Within professional construction crews that work on houses and other buildings, someone is always checking whether all the necessary materials are on hand for at least the next few days of work. This continuous validation improves the efficiency and productivity of the crew, which is to say, their performance, and is essential for delivering results on time and within budget. Build accomplishes the same thing in software. As I said in “From Code to Customer: Exploring Mobile DevOps” (bit.ly/2ayD9Zw), all DevOps activities and practices are means to continually validate the performance of your apps and services. (Again, “performance” means delivering the greatest value to your customers at the lowest cost to your business.) Thus, Build is fundamentally a means to validate the contents of a source code repository, because you expect to have everything in place.
A build (excluding additional tests that might be run) generally has only one of two results: success or failure. Success means that the source repository contains the necessary, buildable files to produce testable and deployable artifacts. Failure means that one or more files are faulty (they have syntax errors), or that something is missing from the repository according to the way the build is configured. (That configuration could be itself faulty, of course.)
Ideally, you want to know as quickly as possible when a defect that “breaks the build” is introduced into the repository. This is where build automation becomes a huge advantage, because you can run a build and get immediate validation whenever there’s a change to the repository. This is known as Continuous Integration.
In times past, builds were often complex, tedious processes that for large projects typically required one or more full-time dedicated employees. For this reason, they were run only infrequently, by which time many hundreds or even thousands of changes might have been committed to the repository. Because any number of those changes, in any combination, might cause the build to fail, it could be a real nightmare to get everyone’s changes integrated into a working build. I even remember a few ambitious projects at Microsoft that were canceled simply because they couldn’t actually be built.
Avoiding such nightmares has given rise to CI. Here’s how I think of it:
- Because Build validates your repository, you naturally want to run builds early and often.
- If you can automate builds, you make the process highly repeatable at very little cost.
- If you can automatically trigger a build whenever there’s a change to the source repository, you’ve achieved CI.
CI, in short, validates each and every change to the source repository as close in time as possible to the change itself, and immediately notifies the appropriate developer if the build fails. Features like gated check-in (with Team Foundation Version Control) or pull request (PR) builder with Git can also trigger a build with the new code before it’s checked in or merged, so that the repository is changed only if the build is successful. Either way, CI quickly detects faulty code in the repository (including code that fails automatically triggered tests, but that’s the subject of a later article).
All this is why build automation with CI is one of the most common DevOps practices. Indeed, even if you do everything else in your release pipeline manually, you’ll find that investing early in source control with automated builds and CI is well worth it, especially as a project becomes more complex.
The Anatomy of Builds
At minimum, Build requires three components: the source code, a build agent and a build definition, which is to say: the code you want to build; a machine that has the necessary tools and SDKs to produce artifacts from that code; and a set of instructions that tell the machine how to go about it. The basic relationships are illustrated in Figure 3. (Again, it’s certainly possible to have multiple repositories, build definitions and build agents, as suggested in Figure 2.)
Figure 3 The Basic Relationships Between a Source Repository, a Build Definition, a Build Agent and the Resulting Build Artifacts
Even if the terms “build agent” and “build definition” seem new, you’ve actually been using them since your very first day of coding with a quintessential “Hello, World!” program:
- By installing some programming tools on your machine, you turned it into a build agent.
- By writing a short program and storing it in a file, you created your first piece of source code.
- By typing in a command to compile and link that code, you created a build definition and ran a build, resulting in a runnable executable. (Modern JIT tools like Microsoft .NET Core typically compile and run the program together.)
Of course, as soon as you got a taste for the magic of coding you started writing a lot more code, factoring that code into multiple files and creating much more complex projects. At that point, typing in commands over and over became tedious, so your build definitions likely took the form of batch files or other scripts. Eventually you also got tired of waiting for everything to recompile every time you ran a build and, thus, discovered the virtues of systems such as NMAKE and MSBuild with makefiles and project files, respectively, serving as build definitions. These systems let you define the interrelationships between files such that you recompile only what’s necessary, greatly shortening your edit-build-test dev loop.
All of this is to say that as software development continues to evolve, so does the sophistication of the build tools you have at your disposal. Most recently, making these tools available as scalable cloud services—what we call Team Foundation Build or TFBuild—has laid the groundwork for the next generation.
Servers, Queues, Agents and Pools
When working by yourself, you’ll gradually install more tools and SDKs on your development machine, making it a richer and more capable agent. Before long, though, it makes sense to create one or more dedicated build servers on which you can cleanly manage the software environment. This is especially important with teams, because it avoids having to keep every developer machine in sync with the necessary tools. Managing such build servers, along with other collaborative tasks like source control, work tracking, and testing, drove the creation of the Microsoft Team Foundation Server (TFS) and TFBuild more than a decade ago.
An important feature of TFBuild, especially with CI, is its ability to manage and coordinate multiple build requests through a queue. If you have many developers committing code throughout the day, many commits will undoubtedly happen when another build is already in progress. But you don’t want to cancel that build, because with CI you want every succeed/fail build report to be associated with a specific commit. At the same time, the more requests that get stacked up in the queue, the longer developers have to wait for build results.
At this point you need to scale the system, meaning that a build agent becomes distinct from a machine. Technically speaking, a build agent is a service: Multiple agents can run on the same physical server, allowing full utilization of multi-core machines by running parallel builds. And when that server is maxed out, you can easily add one or more machines with additional agents.
This creates what’s called an agent pool, an abstraction that refers to the combined power of all the agents, regardless of how they’re distributed across physical machines. TFBuild, in fact, works with agent pools rather than machines. When a build gets to the front of the queue, TFBuild delegates it to the next available agent in the pool. Agent pools can also be shared across different Team Projects, as explained on the “Administer Your Build and Deployment System” page at bit.ly/2b0UwAg.
With mobile app projects, especially, you’ll typically need more than one kind of agent because each target system has a unique set of platform-specific tools and SDKs. Fortunately, Microsoft provides free agents for Windows, OS X, and Linux, as illustrated in Figure 4. Later in this article I’ll demonstrate setting up a Mac agent for iOS builds.
Figure 4 Windows, Mac, and Linux Agents and the Project Types They Can Build
Build Servers in the Cloud
Any conversation about scaling naturally brings up the potential for migrating that computing power to the cloud, where policies and regulations allow. With cloud-based servers, you don’t need to manage the physical machines or even the software environment. Cloud computing is also centered on pay-as-you-go pricing, making it easy and cost-effective to vary your capacity as needed. This is what’s offered through Visual Studio Team Services.
Team Services provides “hosted” build agents. These are Windows machines that are pre-configured with a wide variety of tools and SDKs, as listed on bit.ly/2aNFKis. As of this writing, a hosted agent can build just about anything you can build with Visual Studio and the Java toolchain, including Windows and Android apps written with Xamarin, Apache Cordova, or native tools. You can set up any number of hosted agent pools and organize them as desired into different pricing tiers.
Because the hosted agents run on Windows, however, they can’t build iOS apps or .NET Core/ASP.NET Core apps for Mac or Linux. For these you need to install the OS X or Linux agents on suitable machines and connect those agents to Team Services, where you can then organize them into pools. Similarly, you can install the Windows agent on your own customized machine that includes software not included on the hosted agents. And “machine” here includes virtual machines, as well as those from services like MacinCloud.com. All in all, Team Services really lets you work with any combination of cloud-hosted and on-premises agents.
Automating Builds with Visual Studio Team Services
Let’s now set up an automated TFBuild with continuous integration for a Xamarin app (roughly following “Build Your Xamarin App” at bit.ly/2aiy48y). To begin, create a new Xamarin.Forms project in Visual Studio called Xamarin Build Oct 2016. Next, create a new Team Project for it in your Team Services account called MSDN Magazine Oct 2016, using Git for source control. Then publish the code into the Team Project from within Visual Studio Team Explorer. This results in the code being available in the Team Project code tab on the Team Services portal.
Now set up a build definition for Android by clicking on the Build tab in the Team Project, then clicking + New. This brings up a dialog with a long list of build definition templates for a variety of self-explanatory project types, including native and cross-platform mobile apps. (Note that Apache Cordova projects need an extension from the Team Services marketplace; see bit.ly/2atxNgp for more information.) You can also start with an empty definition and build it up step-by-step.
For this walk-through, select the Xamarin.Android template, click Next to bring up a configuration dialog, and then provide initial settings for the source repository, CI, and the agent queue (all of these can be changed later). Then click Create, and Team Services opens the build definition editor shown in Figure 5. Red text indicates that additional information is required, as you can see with the Build tab.
Before going through the build steps shown in Figure 5, let me summarize what’s on the other tabs (you’ll find the full documentation at bit.ly/2ayghJh):
- Repository is where you connect to repositories outside of your Team Project, such as GitHub, Subversion or any other Git server.
- Triggers set when builds happen, with options for CI, gated check-in and scheduled builds (often used for nightly runs). Note that you can always queue a build manually from Team Services or from within Visual Studio Team Explorer.
- Options | Create Work Item on Failure is how you assign work items to whomever requested a build that fails. When used with CI, the requestor will always be the developer who committed the code that triggered the build, thus making a tight loop between code commits, builds and immediate notifications of failures. (There are also extensions in the Team Services marketplace for sending other types of notifications.)
- Variables let you associate tokens with optionally encrypted values that you can use elsewhere in the build definition, such as credentials. Encrypted values can’t be copied out of the build definition.
- History gives you the change log for the build definition. This is important because the build definition isn’t part of your source repository, and yet changes to the definition can break the build.
Figure 5 The Initial View of a New Xamarin.Android Build Definition
Returning to Figure 5, remember that Build is all about turning source code into the artifacts needed by the rest of the release pipeline, which means applying specific tools to the source code through discrete steps. As you can see, the first step in the Xamarin.Android template is NuGet restore, because you typically don’t add such packages to source control. When the build agent retrieves that code for a build, then, it must restore those packages.
Next, the template includes (as of this writing) steps to activate and deactivate a Xamarin license, which are no longer necessary because Xamarin is now free. It’s safe to delete those steps because they’ll be removed from the template soon enough anyway.
The Build Xamarin.Android Project step is what runs MSBuild on the Android project in the solution. Clicking that step shows the options as in Figure 5, where the More Information link at the bottom takes you to the detailed documentation for the step. Notice the $( ) references to variables, such as $(BuildConfiguration), which is set to “Release” on the Variables tab. Also, Output Directory is where the build definition will place its artifacts for use by other build steps and even other parts of the release pipeline.
The next two steps build any project in the solution that contains the name “test” and then deploys the built app to Xamarin Test Cloud and runs applicable tests. This, along with other test steps added through the + Add build step… command, is how you include test runs in your CI. Because there are no test projects in my solution at this point, I just disable these steps by clearing their Control Options | Enabled checkbox. This way the steps remain in the definition, but won’t be run.
The last two steps, as you can see, sign the Android app package and then publish the artifacts to some other location, if needed. In the latter case, the earlier MSBuild step already stores its results in a Team Services folder, but that’s available only to people with Team Project permissions. A publish or file copy step (there are a number of options in Add new step…) is how you copy artifacts to locations outside of the Team Project.
Running the Build
Once you’ve completed and saved your build definition, click Queue build… to start the process running. This pops up a dialog where you can select the agent queue and the Git branch (if applicable), and even set additional variables and demands for just this one build. Being able to change these parameters lets you easily run a build through a different agent pool without editing the build definition. For example, if you’re in the process of migrating from on-premises agents to hosted agents, you’ll want to manually queue builds to the hosted queue while CI-triggered builds continue to happen on your local agents. When you’re ready to switch over, you then edit the build definition and redirect the CI builds to the hosted agents instead.
In my case, I don’t need to change anything, so I just start the build. This switches to a UI where I can see what steps have been completed, what step is running and the console output directly from the build agent. I have to admit, it’s always interesting to watch a build as it progresses—I’m sure you’ve done that many times! And once the build is done you can download the output for further analysis if needed.
Eventually the build will complete successfully or encounter a failure. Either way, you’ll see a final report, which appears in the list of Completed builds for the definition, as shown in the left side of Figure 6. This is where you can go back and review however many builds are being retained in your Team Project and, of course, queue new builds. This same list appears in the Visual Studio Team Explorer pane, as shown in the right side of Figure 6. From here you can also queue new builds and create new build definitions.
Figure 6 TFBuild Results in Team Services (left) and Visual Studio (right)
I encourage you to play around with this yourself using a new project created from a template in Visual Studio. Especially try checking Continuous Integration in the definition’s Triggers tab, then make some small change in a source file code and commit/push it to the repository. You can then see the new build queued automatically, which appears in the definition’s Queued list in Team Services and in Visual Studio Team Explorer.
Additional TFBuild Steps
A build definition template is obviously just a place to start, and you can just as easily start with an empty definition and add each step individually. Either way, you want to fully familiarize yourself with all of the available steps by opening any build definition and clicking on the Add build step… command. From the Add tasks dialog that appears, you can build a wide variety of custom build definitions. Here’s an overview of what’s available (the current list is always at bit.ly/2biafxz):
- Build tasks: Android (including signing), Ant, CMake, Gradle, Grunt, Gulp, queue a job on a Jenkis server, Maven, MSBuild, SonarQube analysis for MSBuild, Visual Studio Build, Xamarin.Android, Xamarin.iOS, Xcode (build and package).
- Test tasks: Cloud-based load testing with Apache JMeter or Visual Studio Team Services; cloud-based performance testing; publish code coverage and test results; run functional tests (CodedUI or Selenium); Visual Studio Test (including deploying agents); Xamarin Test Cloud.
- Package tasks: Run NuGet and npm tasks, restore Xamarin components, install CocoaPods.
- Deploy tasks: Deploy to Azure Cloud Service, blob, VM, SQL database, WebApp; run Azure PowerShell scripts; run PowerShell or SSH scripts on target machines; manage Azure Resource Groups; deploy Azure Service Fabric applications; copy files to a remote machine.
- Utility tasks: manage zip files, copy/decrypt/delete files; run batch files or command-line tools (including bash and PowerShell); upload files with cURL; and copy files.
There’s also a healthy marketplace of additional tasks to choose from at bit.ly/2aiRnPh, where you’ll find extensions for Azure DevTest Labs, publishing to Google Play, Bower, Docker, HockeyApp, CodePush, FTP, ReactNative, Apache Cordova and much more. Again, spend a little time familiarizing yourself with what’s possible. It’s especially interesting to see tasks for the Google Play marketplace, Docker, HockeyApp and CodePush, among others. Along with the built-in deployment tasks, they demonstrate that you can easily place a task at the end of your build definition to deploy the results to other environments, including testing, staging and even production.
The MyDriving project provides some additional real-world build definition examples. For Xamarin.Android, it uses all of the same steps as discussed in the earlier example and adds a few more to replace tokens, update version names and numbers, use cURL to download a keystore, and deploy to Xamarin Test Cloud. The Universal Windows Platform and iOS build definitions are much the same, just with slightly different steps to manage details like signing certificates. And for the back-end ASP.NET code, MyDriving has a simple definition to run NuGet restore, invoke MSBuild, and publish the artifacts to a server. You can find a step-by-step breakdown of all these build definitions in Chapter 10 of the “MyDriving Reference Guide” at aka.ms/mydrivingdocs.
Deploying an Agent
A distinctive part of any definition is its list of “demands” that you can find on the General tab. The idea of demands is that if you try to queue a build to an agent that doesn’t meet those demands, Team Services can give you an immediate error rather than attempting a hopeless build and making you pick through errors in build output. Thus, a Xamarin.Android definition will list demands like the Android SDK and Xamarin.Android. A build definition for iOS, in contrast, will list demands such as Xcode or Xamarin.iOS.
As noted before, hosted agents in Team Services are Windows machines, but only a Mac can meet the demands for iOS. How, then, do you get a build agent running on such a machine? One way is to use a service like MacinCloud, which is what’s used in MyDriving. There’s a great post on the Visual Studio ALM blog (bit.ly/2ajHtIz) that provides all the details about setting this up.
The other option is to install an agent on your own Mac. Detailed instructions are found on “Deploy an Agent on OS X” (bit.ly/2azm136), but let me share some of that experience here using the humble 5-year-old MacBook that’s sitting on my desk. On that machine I downloaded the OS X agent and started the configuration. This prompted me to enter the URL of my Team Services account, followed by a personal access token obtained from my profile in the Team Services portal. The token is how the agent on the Mac identifies itself, given that a Mac doesn’t otherwise know about my Microsoft account.
Once connected to Team Services, I was prompted for the pool to which to add the agent. As an example, I went to the Agent pool section in Team Services and created “On-Prem Mac,” as shown in Figure 7. A few moments after I gave this name to the agent on the Mac, the agent appeared on Team Services, as shown in the inset.
Figure 7 Creating a New Agent Pool and How a New Agent Appears After Connecting (Inset)
With the agent connected, I made sure to run it on the Mac, which I could do either interactively or as a service. The “Deploy an Agent on OS X” page mentioned earlier describes all this. I then created a Xamarin.iOS build definition in Team Services and selected my Mac-based agent queue for the build (I also chose this in the build definition’s General | Default agent queue setting). Finally, I queued a build through Team Services and—voilà—I see it running on my MacBook and see the build output directly in Team Services, just as with the hosted agent.
A Few Tips and Notes
- Remember that most projects will need a package restore step early in the build definition, such as the NuGet Installer step for Xamarin and .NET projects and the npm step for Apache Cordova projects. Build errors will remind you of this fact!
- By default, each step you add to a build definition will have its Control Options | Continue on error and Always run boxes unchecked. Checking the first means that the step isn’t actually essential for subsequent steps, and shouldn’t fail the build. The second means that the step should always run regardless of what happens with other steps. For example, you might have a step at the end of the definition to copy whatever artifacts were built, even if some didn’t get built, or you might have a specialized cleanup task that should always run.
- When you do manage your own build agents, it’s your responsibility to keep the software up-to-date. Build errors will typically tell you if there’s a version mismatch somewhere.
- Remember to check the Options | Create Work Item on Failure box in the build definition to assign a bug to whoever committed code that triggered a CI build that failed. You can also find additional notification- and e-mail-related tasks in the Team Services marketplace with which to customize your process if you don’t want to rely solely on work items.
- Have an idea for a build extension of your own? Check out the documentation at bit.ly/2avgl97.
As stated earlier in this article, Build is what transforms source code into the artifacts needed by the rest of the release pipeline (even if only for testing purposes). The next article in this series will explore the Release Management features of Visual Studio Team Services, which is how you can define and potentially automate any additional steps that must take place between a build and getting your app and services out to customers. Fortunately, much of what you’ve learned about Build definitions applies also to Release definitions, as the latter are also configured with discrete steps. All of this takes you ever closer to a fully automated release pipeline.
Thanks to the following technical expert for reviewing this article: Andy Lewis
Andy Lewis writes as a senior content developer for Microsoft, covering areas that include Visual Studio Team Services, TFS, and Build. He is passionate about creating powerful and satisfying user experiences in web apps through design integrated with content.