Code

Producing a Zip or Tarball when building a .NET Core console app

There’s a .NET Core console app I’m working on that will need to be published a few different ways. The simplest way to share a downloadable app is in a self-contained tarball (.tar.gz extension) or zip file. Eventually package managers like Chocolatey and rpm/deb files will be needed, but a downloadable, versioned archive file is good enough to get started.

First I did a quick search to see who has solved this problem already. :). One solution I found was qmfrederik/dotnet-packaging on GitHub that looked promising.  It seems like early days, currently version 0.1.45, but it’s published on NuGet under the MIT license so that’s good enough to use as a build tool.

All told – it was quick and easy to integrate – though in my case I needed to push the tool a little to get the results I was looking for. In this post I’ll walk you through that.

Adding this to the .NET Core console app’s .csproj was very easy.

  <ItemGroup>
    <PackageReference Include="Packaging.Targets" Version="0.1.45" />
    <DotNetCliToolReference Include="dotnet-tarball" Version="0.1.45" />
    <DotNetCliToolReference Include="dotnet-zip" Version="0.1.45" />
  </ItemGroup>

Then from a command prompt dotnet zip and dotnet tarball created the archive files I needed for the different target operating systems.

> cd src\Example.CommandLine
> dotnet restore
> dotnet zip --framework netcoreapp2.0 --runtime win10-x64 

Microsoft (R) Build Engine version 15.7.179.6572 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Example.CommandLine -> C:\...\Example\src\Example.CommandLine\bin\Debug\netcoreapp2.0\win10-x64\example.dll
  Example.CommandLine -> C:\...\Example\src\Example.CommandLine\bin\Debug\netcoreapp2.0\win10-x64\publish\
  Creating zip package C:\...\Example\src\Example.CommandLine\bin\Debug\netcoreapp2.0\win10-x64\example.0.1.win10-x64.zip

> dotnet tarball --framework netcoreapp2.0 --runtime linux-x64 

Microsoft (R) Build Engine version 15.7.179.6572 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Example.CommandLine -> C:\...\Example\src\Example.CommandLine\bin\Debug\netcoreapp2.0\linux-x64\example.dll
  Example.CommandLine -> C:\...\Example\src\Example.CommandLine\bin\Debug\netcoreapp2.0\linux-x64\publish\
  Creating tarball C:\...\Example\src\Example.CommandLine\bin\Debug\netcoreapp2.0\linux-x64\example.0.1.linux-x64.zip

In this project I’ve been using VSTS YAML build, and adding dotnet zip and dotnet tarball steps was easy:

- task: DotNetCoreCLI@2
  displayName: dotnet restore
  inputs:
    command: restore
    projects: 'src/Example.CommandLine/Example.CommandLine.csproj'

- task: DotNetCoreCLI@2
  displayName: 'dotnet zip'
  inputs:
    command: custom
    custom: zip
    projects: src/Example.CommandLine/Example.CommandLine.csproj
    arguments: '--framework netcoreapp2.0 --runtime win10-x64 --configuration $(Configuration)'

- task: DotNetCoreCLI@2
  displayName: 'dotnet tarball'
  inputs:
    command: custom
    custom: tarball
    projects: src/Example.CommandLine/Example.CommandLine.csproj
    arguments: '--framework netcoreapp2.0 --runtime linux-x64 --configuration $(Configuration)'

The build already had a Configuration variable set to Release of course.

Changing the output location

That’s where things got a little rocky. One problem that cropped up on the build server was an unexpected error : The project was restored using Microsoft.NETCore.App version 2.0.0, but with current settings, version 2.0.7 would be used instead. The error didn’t happen reliably locally; it seemed to depend on the whether you were using Linux or Windows locally, and which version of the .NET SDK was providing the dotnet command.

The other thing I wanted to do was to produce the two files with the output location in $(Build.ArtifactStagingDirectory) instead of the default bin subfolder inside the source code location. Although Frederik‘s tool doesn’t yet support an --output target location option, it does support being called directly as an msbuild target. That provides the ability to add another property from the command line.

So the command to run becomes this:

dotnet msbuild /t:CreateZip /p:RuntimeIdentifier=win10-x64 /p:TargetFramework=netcoreapp2.0 /p:Configuration=Release

We can also add the Restore target which is a work-around for the strange error that happened on the build server:

dotnet msbuild /t:Restore,CreateZip /p:RuntimeIdentifier=win10-x64 /p:TargetFramework=netcoreapp2.0 /p:Configuration=Release

Lastly we can add an additional msbuild property that we want to control the target path:

dotnet msbuild /t:Restore,CreateZip /p:ArchiveDir=$(Build.ArtifactStagingDirectory)/downloads /p:RuntimeIdentifier=win10-x64 /p:TargetFramework=netcoreapp2.0 /p:Configuration=Release

The made-up ArchiveDir property is used instead of PublishDir or TargetDir because I didn’t want have the other side-effects caused by changing those existing properties. (Aside, how cool is it that you can hyperlink to the MsBuild and .NET SDK targets as source code now?)

Adding custom msbuild tricks to a repo

Now that we’ve said where we want the packages to be be created – we need to add a bit of custom msbuild xml do adjust a few properties at just the right instant. Taking a look at the targets file in the tool we can see there’s a PackagePath property we want to update.

To make that happen I like to add a Directory.Build.targets file at the root of the repo to add custom msbuild. It keeps the original csproj cleaner. Here’s what it contained:

<Project>
  <PropertyGroup>
    <MsBuildAllProjects>$(MsBuildAllProjects);$(MsBuildThisFileFullPath)</MsBuildAllProjects>
  </PropertyGroup>

  <Target Name="FixPackageProperties" AfterTargets="CreatePackageProperties">
    <PropertyGroup>
      <PackageName>$(PackagePrefix)-$(PackageVersion)-$(RuntimeIdentifier)</PackageName>
      <_ArchiveDir>$(ArchiveDir)</_ArchiveDir>
      <_ArchiveDir Condition=" '$(_ArchiveDir)' == '' ">$(TargetDir)</_ArchiveDir>
      <_ArchiveDir Condition=" '$(_ArchiveDir)' != '' AND !HasTrailingSlash('$(_ArchiveDir)') ">$(_ArchiveDir)\</_ArchiveDir>
      <PackagePath Condition=" '$(_ArchiveDir)' != '' ">$(_ArchiveDir)$(PackageName)</PackagePath>
    </PropertyGroup>
    <MakeDir Directories="$(_ArchiveDir)" Condition=" '$(_ArchiveDir)' != '' " />
  </Target>
</Project>

The important thing this does is change the PackagePath property after the CreatePackageProperties target runs if there is an ArchiveDir property provided. It also changes the PackageName to use use dashes instead of dots to delimit the different parts of the file name, but that’s just personal preference.

Once that was in place – the build yaml was updated to the following – and the CI server began producing .zip and .tar.gz files and each build pushed them its artifacts drop.

- task: DotNetCoreCLI@2
  displayName: dotnet restore
  inputs:
    command: restore
    projects: 'src/Example.CommandLine/Example.CommandLine.csproj'

- task: DotNetCoreCLI@2
  displayName: 'dotnet tarball'
  inputs:
    command: custom
    custom: msbuild
    projects: src/Example.CommandLine/Example.CommandLine.csproj
    arguments: '/t:Restore,CreateTarball /p:RuntimeIdentifier=linux-x64 /p:TargetFramework=netcoreapp2.0 /p:Configuration=Release /p:ArchiveDir=$(Build.ArtifactStagingDirectory)/downloads'

- task: DotNetCoreCLI@2
  displayName: 'dotnet zip'
  inputs:
    command: custom
    custom: msbuild
    projects: src/Example.CommandLine/Example.CommandLine.csproj
    arguments: '/t:Restore,CreateZip /p:RuntimeIdentifier=win10-x64 /p:TargetFramework=netcoreapp2.0 /p:Configuration=Release /p:ArchiveDir=$(Build.ArtifactStagingDirectory)/downloads'

- task: PublishBuildArtifacts@1
  displayName: publish artifacts
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'

The condition on the last task is because this yaml build file is used for CI build on master branch as well as PR build on pull request branches. PR from unknown sources aren’t necessarily trustworthy (because internet) the agent pool will be different and the build outputs themselves won’t leave the build agents’ temporary folder. Only the log files and pass/fail status leave the PR-build machine boundary.

One thought on “Producing a Zip or Tarball when building a .NET Core console app

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.