blog post image
Andrew Lock avatar

Andrew Lock

~5 min read

Building ASP.NET Core apps on both Windows and Linux using AppVeyor

Nearly two years ago I wrote a post about using AppVeyor to build and publish your first .NET Core NuGet package. A lot has changed with ASP.NET Core since then, but by-and-large the process is still the same. I've largely moved to using Cake to build my apps, but I still use AppVeyor, with the appveyor.yml file essentially unchanged, short of updating the image to Visual Studio 2017.

Recently, AppVeyor announced the general availability of AppVeyor for Linux. This is a big step, previously AppVeyor was Windows only so you had to use a different service if you wanted CI on Linux (I have been using Travis). While Travis has been fine for my needs, I find it noticeably slower to start than AppVeyor. It would also be nice to consolidate on a single CI solution.

In this post, I'll take an existing appveyor.yml file, and update it to build on both Windows and Linux with appveyor. For this post I'm only looking at building .NET Core projects, i.e. I'm not targeting the full .NET Framework at all.

The Windows only AppVeyor build script

AppVeyor provides several ways to configure a project for CI builds. The approach I use for projects hosted on GitHub, described in my previous post, is to add an appveyor.yml file in the root of my GitHub repository. This file also disables the automatic detection AppVeyor can perform to try and build your project automatically, and instead provides a build script for it to use.

The build script in this case is very simple. As I said previously, I tend to use Cake for my builds these days, but that's somewhat immaterial. The important point is we have a build script that AppVeyor can run to build the project.

For reference, this is the script I'll be using. The Exec function ensures any errors are bubbled up correctly. Otherwise I'm litteraly just calling dotnet pack in the root directory (which does an implicit dotnet restore and dotnet test)

function Exec  
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd,
        [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd)
    )
    & $cmd
    if ($lastexitcode -ne 0) {
        throw ("Exec: " + $errorMessage)
    }
}

if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse }

$revision = @{ $true = $env:APPVEYOR_BUILD_NUMBER; $false = 1 }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL];
$revision = "beta-{0:D4}" -f [convert]::ToInt32($revision, 10)

exec { & dotnet pack .t -c Release -o .\artifacts --version-suffix=$revision }  

The appveyor.yml file to build the app is shown below. The important points are:

  • We're using the Visual Studio 2017 build image
  • Only building the master branch (and PRs to it)
  • Run Build.ps1 to build the project
  • For tagged commits, deploy the NuGet packages to www.nuget.org
version: '{build}'
image: Visual Studio 2017
pull_requests:
  do_not_increment_build_number: true
branches:
  only:
  - master
nuget:
  disable_publish_on_pr: true
build_script:
- ps: .\Build.ps1
test: off
artifacts:
- path: .\artifacts\**\*.nupkg
  name: NuGet
deploy:
- provider: NuGet
  name: production
  api_key:
    secure: nyE3SEqDxSkfdsyfsdjmfdshjk767fYuUB7NwjOUwDi3jXQItElcp2h
  on:
    branch: master
    appveyor_repo_tag: true

You can see an example of this configuration in a test GitHub repository (tagged 0.1.0-beta)

Updating the appveyor.yml to build on Linux

Now you know what we're starting from, I'l update it to allow building on Linux. The getting started guide on AppVeyor's site is excellent, so it didn't take me long to get up and running.

I've listed the final appveyor.yml at the end of this post, but I'll walk through each of the changes I made to the previous appveyor.yml to get builds working. Applying this to your own appveyor.yml will hopefully just be a case of working through each step in turn.

1. Add the ubuntu image

AppVeyor let you specify multiple images in your appveyor.yml. AppVeyor will run builds against each image; every configuration must pass for the overall build to pass. In the previous configuration, I was using a single image, Visual Studio 2017:

image: Visual Studio 2017  

You can update this to use Ubuntu too, by using a list instead of a single value:

image: 
  - Visual Studio 2017
  - Ubuntu

Remember: YAML is sensitive to both case and white-space, so be careful when updating your appveyor.yml!

2. Update your build script (optional)

The ubuntu image comes pre-installed with a whole host of tools, one of which is PowerShell Core. That means there's a strong possibility that your PowerShell build script (like the one I showed earlier) will work on Linux too!

For me, this example of moving from Windows to Linux and having your build scripts just work, is one of the biggest selling points for PowerShell Core.

However, if your build scripts don't work on Linux, you might need to run a different script on Linux than in Windows. You can achieve this by using different prefixes for each environment: ps for Windows, sh for Linux. You also need to tell AppVeyor not to try and run the PowerShell commands on linux.

Previously, the build_script section looked like this:

build_script:
- ps: .\Build.ps1

Updating to run a bash script on Linux, and setting the APPVEYOR_YML_DISABLE_PS_LINUX environment variable, the whole build section looks like this:

environment:  
  APPVEYOR_YML_DISABLE_PS_LINUX: true

build_script:  
- ps: .\Build.ps1
- sh: ./build.sh

Final tip - remember, paths in Linux are case sensitive, so make sure the name of your build script matches the actual name and casing of the real file path. Windows is much more forgiving in that respect!

3. Conditionally deploy artifacts (optional)

As part of my CI process, I automatically push any NuGet packages to MyGet/NuGet when the build passes if the commit has a tag. That's handled by the deploy section of appveyor.yml.

However, if we're running appveyor builds on both Linux and Windows, I don't want both builds to try and push to NuGet. Instead, I pick just one to do so (in this case Ubuntu - either will do).

To conditionally run sections of appveyor.yml, you must use the slightly-awkward "matrix specialisation" syntax. That turns the deploy section of appveyor.yml from this:

deploy:
- provider: NuGet
  name: production
  api_key:
    secure: nyE3SEqDxSkHrLGAQJBMh2Oo6deEnWCEKoHCVafYuUB7NwjOUwDi3jXQItElcp2h
  on:
    branch: master
    appveyor_repo_tag: true

to this:

for:
-
  matrix:
    only:
      - image: Ubuntu

  deploy:
  - provider: NuGet
    name: production
    api_key:
      secure: nyE3SEqDxSkHrLGAQJBMh2Oo6deEnWCEKoHCVafYuUB7NwjOUwDi3jXQItElcp2h
    on:
      branch: master
      appveyor_repo_tag: true

Important points:

  • Remember, YAML is case and whitespace sensitive
  • Add the for/matrix/only section
  • Indent the whole deploy section so it is level with matrix.

That last point is critical, so here it is in image form, with indent guides:

Indent the whole deploy section so it is level with matrix

And that's all there is to it - you should now have cross-platform builds!

Successful build status in appveyor

The final appveyor.yml for multi-platform builds

For completion sake, a complete, combined appveyor.yml is shown below. This differs slightly from the example in my test repository on GitHub but it highlights all of the features I've talked about.

version: '{build}'  
image: 
  - Visual Studio 2017
  - Ubuntu
pull_requests:  
  do_not_increment_build_number: true
branches:  
  only:
  - master
nuget:  
  disable_publish_on_pr: true

environment:  
  APPVEYOR_YML_DISABLE_PS_LINUX: true
build_script:  
- ps: .\Build.ps1
- sh: ./build.sh

test: off  
artifacts:  
- path: .\artifacts\**\*.nupkg
  name: NuGet

for:
-
  matrix:
    only:
      - image: Ubuntu

  deploy:
  - provider: NuGet
    name: production
    api_key:
      secure: nyE3SEqDxSkHrLGAQJBMh2Oo6deEnWCEKoHCVafYuUB7NwjOUwDi3jXQItElcp2h
    on:
      branch: master
      appveyor_repo_tag: true

Summary

Appveyor recently announced the general availability of Appveyor for Linux. This means you can now run CI builds on AppVeyor, whereas previously you were limited to Windows only. If you wish to run builds on both Windows and Linux, you need to update your appveyor.yml to handle the fact that the configuration is controlling two different builds.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?