How use Visual Studio, MSBuild or Roslyn previews in GitHub or DevOps CI

If you want to leverage the many awesome C# 9 features, including roslyn source generators like ThisAssembly, all of which require the latest and greatest .NET 5.0 preview, it would be a pity to have to give up the safety net of your CI builds (whether GitHub Workflows or Azure DevOps pipelines) just because they don’t provide hosted images with the relevant bits.

This post shows how to install and use the latest Visual Studio preview from your build script.

Yes, it might just be enough to install the .NET Core RC and use dotnet build and dotnet test. In some cases you do need a full Visual Studio depending on your project.

The key to enabling this scenario is a little awesome (if I might say so) dotnet global tool called dotnet-vs: “A global tool for running, managing and querying Visual Studio installations”. It’s a cool little thing Adrian Alonso and myself created to more easily manage multiple versions of Visual Studio installed side by side. It can get quite crazy at times.

The tool allows, among other things, to query installed VS versions and install new ones, including adding/removing components. It internally uses vswhere as well as the Visual Studio installer command line to achieve a seamless experience.

So, on to the actual scripts that are really quite simple.

GitHub Workflow

The whole build workflow (which you can see in action too) is:

name: build
on: push

jobs:
  build:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-dotnet@v1
        with:
          dotnet-version: 3.1.x
      - run: dotnet tool update -g dotnet-vs
      - run: echo "::set-env name=MSB::$(vs where preview --prop=InstallationPath)"
      - run: vs install preview --quiet +Microsoft.VisualStudio.Component.ManagedDesktop.Core +Microsoft.NetCore.Component.DevelopmentTools
        if: env.MSB == ''
      - run: echo "::add-path::$(vs where preview --prop=InstallationPath)\MSBuild\Current\Bin"
      - run: msbuild -r
      - run: msbuild -t:test

Relevant steps:

  1. Install/update to latest & greatest dotnet-vs by simply using dotnet tool update -g. That will install the tool if it’s not there, and ensure it’s the latest otherwise. I do this because if VS preview requires some newer command args in the future, the latest dotnet-vs tool will likely support that too.

  2. The syntax for setting an environment from a GH action is a bit weird, but the notable thing here is that the run command will actually run Powershell Core by default, unlike on DevOps where it runs cmd.exe (on Windows agents, in both cases):

    RunPwsh.png

    So we take advantage of that fact and just run the vs where command inline to set the value of the installation directory for a preview version of VS. The dotnet-vs tool where command will return the raw value from that execution, or an empty string if no such version is found.

  3. We use that as the condition for the vs install so that we only do so if the preview isn’t there already. Note how you can add any supported workload or component ID to the installation with the simple +[ID] syntax. There are also shorter aliases for common workloads like +core +desktop +azure +mobile, say. The ones I’m installing in this case are just the minium I need, so I can get the install in just about ~5 minutes!

  4. We finally use the same “trick” as step 2 for adding the MSBuild path to the %PATH% so that we can finally just run msbuild.

All in all, pretty straightforward and concise. I love it how GitHub run actions are rendered by default using the frst line of the command. I wish Azure DevOps did the same, instead of showing just CmdLine and forcing you to always annotate steps with displayName.

Azure DevOps

The whole build pipeline (which you can see in action too) is:

pool:
  vmImage: 'windows-2019'
steps:
- checkout: self

- task: UseDotNet@2
  inputs:
    packageType: sdk
    version: 3.1.x
    performMultiLevelLookup: true

- script: dotnet tool update -g dotnet-vs
- pwsh: echo "##vso[task.setvariable variable=MSB]$(vs where preview --prop=InstallationPath)"
- script: vs install preview --quiet +Microsoft.VisualStudio.Component.ManagedDesktop.Core +Microsoft.NetCore.Component.DevelopmentTools
  condition: eq(variables['MSB'], '')
- pwsh: echo "##vso[task.prependpath]$(vs where preview --prop=InstallationPath)\MSBuild\Current\Bin"
- script: msbuild -r
- script: msbuild -t:test

(I removed all the displayName for conciseness).

You can see that the structure is pretty much the same as for GitHub workflows. Note that we need to explicitly choose to run with powershell by using pwsh instead of script, so that the inline execution of vs commands when expanding the string for the variables works the same way. We use the ##vso[task.XXX] syntax in this case instead.

The condition syntax in GitHub workflows is also so much nicer :).

And that is all you need to install quickly (both in ~5’ in this combination of components) and build in CI using the latest and greatest C# features!