Packaging of a Mono application on Linux

How to package a Mono (.NET) applications for Debian-based Linux in command line. Packaging process can be easily automated on Continues Integration Server.

debian_package

Package building workflow is quite simple:

  • Assign a version number to a .NET assembly code
  • Build binaries
  • Copy binaries to package folder and build deb-package with the same version as an assembly

Build process will be controlled by MSBuild project file and run from a continues integrations server. MSBuild on Mono platform is substituted by xbuild utility.

Sample solution is available for download here https://github.com/mchudinov/PackagingMono. Solution is compatible with Visual Studio 2012, MonoDevelop/Xamarin Studio 5.

This is my third blogpost about automation of development workflow with Mono. Automated building and versioning were covered in my previous posts:

I need to create a Debian package from my .NET binaries. The result should be a deb-package that satisfy the following requirements:

  • Package version is the same as an assembly version of an application
  • While installation checks if another version of the same package is already installed
  • Requires Mono package. If there is no Mono installed then installation fails
  • Installs binary into predefined folder. I use /opt/{MyProgramName}
  • While installation checks if a old configuration file is present and asks user to overwrite or keep it

Official Debian packaging documentation is available here Debian Policy Manual – Binary Packages. I will keep the packaging process as simple and minimalistic as possible.

1. Prepare packaging environment
Use any Debian/Ubuntu-flavour Linux (I use Mint). A couple of programs are needed on build server to build deb packages. Let’s install them first:

$ sudo apt-get install dpkg debconf debhelper 
$ sudo apt-get install fakeroot rsync dos2unix
$ sudo apt-get install lintian

dos2unix is a converter from DOS to UNIX text file format. I will need this program when assign a version number to my package.
fakeroot runs a command in an environment wherein it appears to have root privileges for file manipulation. It will be used for packaging.
rsync is a file copying tool. It will be used to copy files into package folder structure.
Lintian is optional. This program helps to identify bugs in Debian packages.

2. Prepare folders
Add a folder called Package to root of the C# solution and create the following structure inside it

mike@mike-machine $ tree Package
Package
└── deb
    ├── DEBIAN
    │   ├── conffiles
    │   ├── control
    │   └── preinst
    └── opt
        └── PackagingMono

4 directories, 3 files

Folder deb is placed just inside the Package folder. This is package root. There are more folders inside the package root: folder DEBIAN (in capital letters!) with two files control and conffiles. Folder DEBIAN contains files that are used while installation.

And another folder in my package root is opt. This is because I want to install my program to /opt folder of Linux operating system.

While installation all folders from package root will be copied to a Linux OS root. Thus folder structure must be exactly the same as we want it to be after installation. I will install my program to /opt/PackagingMono. Where PackagingMono is the name of my application. Thus I have /deb/opt/PackagingMono in my package folder structure.

DEBIAN/control file
This is the only file that is required to create a package. All the other files inside DEBIAN folder are optional. control file has mandatory fields, recommended fields and optional fields:

Attribute Description Status Examples
Package The name of the binary package. [a-zA-Z0-9-] – only Latin letters, numbers and dash. This name is used in installation: apt-get install {package} mandatory Package: PackagingMono
Version The version number of a package. Version is used while installation to determine should package be updated or not. mandatory Version: 1.0-1
Version: 2009.12.12-1
Architecture A unique single word identifying a Debian machine architecture or an architecture wildcard. Possible values: i386, amd64, all, source. mandatory Architecture: all
Architecture: amd64
Maintainer The package maintainer’s name and email address. The name must come first, then the email address inside angle brackets <> mandatory Maintainer:Mikael Chudinov <mikael@chudinovnet>
Description A description of the binary package, consisting of two parts, the synopsis or the short description, and the long description. It is a multiline field with the following format:Description: <single line synopsis>
<extended description over several lines>
mandatory Description: Short.
␣Long
␣goes here.
␣.
␣New line.
Section This field specifies an application area into which the package has been classified. Possible values: admin, base, comm, contrib, devel, doc, editors, electronics, embedded, games, gnome, graphics, hamradio, interpreters, kde, libs, libdevel, mail, math, misc, net, news, non-free, oldlibs, otherosfs, perl, python, science, shells, sound, tex, text, utils, web, x11 recommended Section: misc
Priority This field represents how important it is that the user have the package installed. Possible values: extra, optional, standard, important, required (these can not be uninstalled!) recommended Priority: optional
Depends Relationships to other packages Depends: dpkg, libz (>= 1.2.3), jpeg (= 6b), png (< 2.0)

More information in Debian documentation Binary package control files — DEBIAN/control.

Here is a simple DEBIAN/control file for a Mono application:

Package:packagingmono
Version:1.0
Maintainer:Mikael Chudinov <mikael@chudinov.net>
Architecture:amd64
Section:net
Description:Template for Debian packaged Mono application.
Depends:mono-complete (>=3)

 

DEBIAN/conffiles contains a list of configuration files
Here is a section of Debian manual about configuration files Configuration files. If a configuration file is listed in DEBIAN/conffiles then local changes will be preserved during a package upgrade, and user will be asked about to keep or overwrite changes.

Manual says about location of configuration files that “Any configuration files created or used by your package must reside in /etc“. But for simplicity we place configuration file in the same folder as binary. The name template of a config file is {ProgramName}.exe.config.

My conffiles for this project contains just one line

/opt/PackagingMono/PackagingMono.exe.config

 

DEBIAN/(preinst|postinst|prerm|postrm) installation scripts
There are 4 installation scripts may be used in a package:

Script Description
DEBIAN/preinst This script will be executed before the package installation. It should prepera the environment for successful installation. I use it to install mono.
DEBIAN/postinst It will be executed right after installation.
DEBIAN/prerm This script will be executed before package removal.
DEBIAN/postrm This script will be executed right after package removal.

My simple DEBIAN/preinst script for a Mono-based application installs Mono framework:

#!/bin/bash
apt-get install mono-complete
exit 0

 

gitignore (hgignore)
Ignore files are not needed for packaging itself. But we need them to submit folder structure to Git/Hg source control. One -ignore file should be placed right in the /deb folder:

# Ignore
*.deb

# But not these files...
!.gitignore
!/deb/**/*

And another one should be placed in installation folder. This is /opt/PackagingMono in my case:

# Ignore everything
*

# But not these files...
!.gitignore
!*.sh

 

3. MSBuild building script file
Packaging is a part of a application building process. I control building process through a single MSBuild project file. This project file is used by continues integration server. Building file  contains the following simple steps (targets in MSBuild terminology):

  • Clean project
  • Restore NuGet packages
  • Assign version to the assembly
  • Build binaries
  • Packaging with following substeps:
  • Set version number to deb-package in DEBIAN/control
  • Copy binaries to package root
  • Start packaging process

Restoring NuGet packages, building binaries, testing and versioning of assembly steps were covered in my previous blogposts Building and Testing and Versioning.

My example MSBuild file

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Run">   

    <PropertyGroup>
	    <Configuration>Release</Configuration>
        <SolutionName>PackagingMono</SolutionName>
	</PropertyGroup>

    <Target Name="Run">
	 <CallTarget Targets="Clean" />
	 <CallTarget Targets="Restore" />
	 <CallTarget Targets="Version" />
	 <CallTarget Targets="Build" />
         <CallTarget Targets="Pack" />
    </Target>
    
    <Target Name="Clean">
	 <Message Text="Clean" />
	 <RemoveDir Directories="$(SolutionName)/bin; Test/bin;" ContinueOnError="False"/>
     <RemoveDir Directories="$(SolutionName)/obj; Test/obj;" ContinueOnError="False"/>
    </Target>
    
    <Target Name="Restore">
	 <Message Text="Restore NuGet packages" />
     <Exec Command="nuget.exe restore" ContinueOnError="False"/>
    </Target>
  
  	<UsingTask AssemblyFile="packages/MSBuild.Extension.Pack.1.5.0/tools/net40/MSBuild.ExtensionPack.dll" TaskName="AssemblyInfo"/>
	<Target Name="Version">
		<Message Text="Versioning assemblies" />
	
		<ItemGroup>
		  <AssemblyInfoFiles Include="**\AssemblyInfo.cs" />
		</ItemGroup>
	
		<AssemblyInfo
			AssemblyInfoFiles="@(AssemblyInfoFiles)"
			
			AssemblyMajorVersion="$(MajorVersion)"
			AssemblyMinorVersion="$(MinorVersion)"
			AssemblyBuildNumberType="DateString"
			AssemblyBuildNumberFormat="MMdd"
			AssemblyRevisionType="AutoIncrement"
			AssemblyRevisionFormat="000"
		  
			AssemblyFileMajorVersion="$(MajorVersion)"
			AssemblyFileMinorVersion="$(MinorVersion)"
			AssemblyFileBuildNumberType="DateString"
			AssemblyFileBuildNumberFormat="MMdd"
			AssemblyFileRevisionType="AutoIncrement"
			AssemblyFileRevisionFormat="000"
		/>
	</Target>
  
    <Target Name="Build">
	  <Message Text="Build $(Configuration)" />
	  <MSBuild Projects="$(SolutionName)/$(SolutionName).csproj" Properties="Configuration=$(Configuration)" ContinueOnError="False"/>   
    </Target>
	
    <UsingTask AssemblyFile="packages/MSBuild.Extension.Pack.1.5.0/tools/net40/MSBuild.ExtensionPack.dll" TaskName="Assembly"/>
    <UsingTask AssemblyFile="packages/MSBuildTasks.1.4.0.78/tools/MSBuild.Community.Tasks.dll" TaskName="FileUpdate"/>
    <Target Name="Pack" Condition=" '$(OS)' == 'Unix'" >
		<Message Text="Pack binaries to deb package" />
		<PropertyGroup>
			<BinaryFolder>$(SolutionName)/bin/$(Configuration)</BinaryFolder>
			<PackageFolder>Package</PackageFolder>
			<TempFolder>temp</TempFolder>
		<PackageDebFolder>$(PackageFolder)/deb</PackageDebFolder>
			<PackageTempFolder>$(PackageFolder)/$(TempFolder)</PackageTempFolder>
		<MainExecutable>$(BinaryFolder)/$(SolutionName).exe</MainExecutable>
		</PropertyGroup>
		
		<RemoveDir Directories="$(PackageTempFolder)" />    
		<ItemGroup>
			<FilesToDeleteInPackageFolder Include="$(PackageFolder)/*.deb"/>
		</ItemGroup>    
		<Delete Files="@(FilesToDeleteInPackageFolder)"/>
	
		<MakeDir Directories="$(PackageTempFolder)"/>
		
		<Exec Command="rsync -r --delete $(PackageDebFolder)/* $(PackageTempFolder)"/>
	
		<Assembly TaskAction="GetInfo" NetAssembly="$(MainExecutable)"> 
			<Output TaskParameter="OutputItems" ItemName="Info"/> 
		</Assembly>
		<Message Text="AssemblyVersion: %(Info.AssemblyVersion)" />
	
		<FileUpdate Files="$(PackageTempFolder)/DEBIAN/control"
					Regex="{xxx}"
					ReplacementText="%(Info.AssemblyVersion)" />    
	
		<Exec Command="dos2unix $(PackageTempFolder)/DEBIAN/control"/>
		
		<ItemGroup>
			<BinaryFiles Include="$(BinaryFolder)/**/*.dll;$(BinaryFolder)/**/*.exe;$(BinaryFolder)/**/*.config"/>
		</ItemGroup>
		<Copy SourceFiles="@(BinaryFiles)" DestinationFolder="$(PackageTempFolder)/opt/$(SolutionName)"/>
	
		<Exec Command="fakeroot dpkg-deb -v --build $(PackageTempFolder)"/>
		<Copy 
			SourceFiles="$(PackageFolder)/$(TempFolder).deb" 
			DestinationFiles="$(PackageFolder)/$(SolutionName)_%(Info.AssemblyVersion)_amd64.deb"/>
		<Delete Files="$(PackageFolder)/$(TempFolder).deb"/>
	
		<RemoveDir Directories="$(PackageTempFolder)" />
	</Target>

</Project>

 

4. Copy binaries to package root
I want to install my program to /opt/PackagingMono folder. Thus I have /deb/opt/PackagingMono folder in my package folder structure. My program files must be placed in the package folder structure after building of assemblies and before packaging. This is a separated Copy step in my Build.proj MSBuild project file. I use a temporary folder to operate on it while the original Package/deb folder stays untouched.

Workflow:

  • Delete old deb package and old temporary folder if present
  • Create a new temporary folder Package/temp
  • Copy package structure from Package/deb to a temporary folder Package/temp using rsync
  • Copy all program files from …/bin/Release folder to Package/temp/opt/{MyProgramName} using Copy MSBuild task.

Here is a part of build script responsible for files copy:

<PropertyGroup>
	<BinaryFolder>$(SolutionName)/bin/$(Configuration)</BinaryFolder>
	<PackageFolder>Package</PackageFolder>
	<TempFolder>temp</TempFolder>
    <PackageDebFolder>$(PackageFolder)/deb</PackageDebFolder>
	<PackageTempFolder>$(PackageFolder)/$(TempFolder)</PackageTempFolder>
</PropertyGroup>

<RemoveDir Directories="$(PackageTempFolder)" />    
<ItemGroup>
	<FilesToDeleteInPackageFolder Include="$(PackageFolder)/*.deb"/>
</ItemGroup>    
<Delete Files="@(FilesToDeleteInPackageFolder)"/>

<MakeDir Directories="$(PackageTempFolder)"/>

<Exec Command="rsync -r --delete $(PackageDebFolder)/* $(PackageTempFolder)"/>

<ItemGroup>
	<BinaryFiles Include="$(BinaryFolder)/**/*.dll;$(BinaryFolder)/**/*.exe;$(BinaryFolder)/**/*.config"/>
</ItemGroup>
<Copy SourceFiles="@(BinaryFiles)" DestinationFolder="$(PackageTempFolder)/opt/$(SolutionName)"/>

 

5. Versioning
I like when my packages have the same version number as .NET assemblies it contains. Versioning of an assembly using an MSBuild task was covered in my previous post about Versioning.

Version number in ../Package/deb/DEBIAN/control file must be a pattern string, not a real number. Patter string can be for instance Version:{xxx}  The patter string will be changed to real version number by MSBuild script in temporary ../Package/temp/DEBIAN/control file.

Package:packagingmono
Version:{xxx}
Maintainer:Mikael Chudinov <mikael@chudinov.net>
Architecture:amd64
Section:net
Description:Template for Debian packaged Mono application.
Depends:mono-complete (>=3)

Workflow:

  • Read version number from assembly using Assembly target from MSBuild.Extension.Pack
  • Update version number in temporary DEBIAN/control file using pattern {xxx} and FileUpdate target from MSBuild Community Tasks
  • Run dos2unix to change line ending to unix format in DEBIAN/control. It’s needed because FileUpdate task changes line endings in control file to Windows style after which dpkg utility can not use it.

Part of my build script that will change versio number in DEBIAN/control file.

<Assembly TaskAction="GetInfo" NetAssembly="$(MainExecutable)"> 
    <Output TaskParameter="OutputItems" ItemName="Info"/> 
</Assembly>
<Message Text="AssemblyVersion: %(Info.AssemblyVersion)" />

<FileUpdate Files="$(PackageTempFolder)/DEBIAN/control"
            Regex="{xxx}"
            ReplacementText="%(Info.AssemblyVersion)" />    

<Exec Command="dos2unix $(PackageTempFolder)/DEBIAN/control"/>

 

6. Build a package!
Now all the files are placed where they should be. It’s time to build a package! First of all files and folders in the package must have root:root group:user access. The easiest way to achieve this is to use fakeroot command:

$ fakeroot dpkg-deb -v --build Package/temp

Workflow:

  • Create the package using dpkg-deb and fakeroot
  • Rename created deb file to a file with version number in name. Just copy created xxx.deb file to {SolutionName}_{Version}_{Architecture}.deb. {Architecture} is optional and can be amd64 for instance. It should be the same as Architecture parameter used in DEBIAN/config file.
  • Delete temporary ../Package/temp folder
<Exec Command="fakeroot dpkg-deb -v --build $(PackageTempFolder)"/>
<Copy 
	SourceFiles="$(PackageFolder)/$(TempFolder).deb" 
	DestinationFiles="$(PackageFolder)/$(SolutionName)_%(Info.AssemblyVersion)_amd64.deb"/>
<Delete Files="$(PackageFolder)/$(TempFolder).deb"/>

<RemoveDir Directories="$(PackageTempFolder)" />

Done! That is all we need to build a simple binary .NET/Mono application package by continues integration server. xbuild runs Build.proj file and creates deb-package in {solutionroot}/Package folder.

Let’s run build script from command line:

mike@mike-machine $ xbuild Build.proj 
XBuild Engine Version 12.0
Mono, Version 3.2.8.0
Copyright (C) 2005-2013 Various Mono authors

Build started 16.12.2014 1:00:02.
__________________________________________________
Project "/home/mike/projects/PackagingMono/Build.proj" (default target(s)):
	Target Run:
		Target Clean:
			Clean
		Target Restore:
			Restore NuGet packages
			Executing: nuget.exe restore
			Все пакеты, перечисленные в packages.config, уже установлены.
			Все пакеты, перечисленные в packages.config, уже установлены.
		Target Version:
			Versioning assemblies
		Target Build:
			Build Release
			Project "/home/mike/projects/PackagingMono/PackagingMono/PackagingMono.csproj" (default target(s)):
				Target PrepareForBuild:
					....
				Target CoreCompile:
					....
				Target _CopyAppConfigFile:
					....
				Target DeployOutputFiles:
					....
			Done building project "/home/mike/projects/PackagingMono/PackagingMono/PackagingMono.csproj".
		Target Pack:
			Pack binaries to deb package
			Deleting file '/home/mike/projects/PackagingMono/Package/PackagingMono_1.0.1216.2_amd64.deb'
			Created directory "Package/temp"
			Executing: rsync -r --delete Package/deb/* Package/temp
			Copying file from '/home/mike/projects/PackagingMono/PackagingMono/bin/Release/PackagingMono.exe' to '/home/mike/projects/PackagingMono/Package/temp/opt/PackagingMono/PackagingMono.exe'
			Copying file from '/home/mike/projects/PackagingMono/PackagingMono/bin/Release/PackagingMono.exe.config' to '/home/mike/projects/PackagingMono/Package/temp/opt/PackagingMono/PackagingMono.exe.config'
			Loading Assembly: /home/mike/projects/PackagingMono/PackagingMono/bin/Release/PackagingMono.exe
			Identity: PackagingMono
			FullName: PackagingMono, Version=1.0.1216.5, Culture=neutral, PublicKeyToken=null
			FileVersion: 1.0.1216.5
			AssemblyVersion: 1.0.1216.5
			Updating File "Package/temp/DEBIAN/control".
			Executing: dos2unix Package/temp/DEBIAN/control
			dos2unix: преобразование файла Package/temp/DEBIAN/control в формат Unix ...
			Executing: fakeroot dpkg-deb -v --build Package/temp
			dpkg-deb: сборка пакета «packagingmono» в файл «Package/temp.deb».
			Copying file from '/home/mike/projects/PackagingMono/Package/temp.deb' to '/home/mike/projects/PackagingMono/Package/PackagingMono_1.0.1216.5.deb'
				Deleting file '/home/mike/projects/PackagingMono/Package/temp.deb'
Done building project "/home/mike/projects/PackagingMono/Build.proj".

Build succeeded.
	 0 Warning(s)
	 0 Error(s)

Time Elapsed 00:00:02.2829220

Package that was just created can be checked by Lintian program:

mike@mike-machine $ lintian ./Package/PackagingMono_1.0.1216.5.deb 
E: packagingmono: changelog-file-missing-in-native-package
E: packagingmono: non-etc-file-marked-as-conffile opt/PackagingMono/PackagingMono.exe.config
E: packagingmono: no-copyright-file
E: packagingmono: extended-description-is-empty
W: packagingmono: no-priority-field
E: packagingmono: depends-on-metapackage depends: mono-complete (>=3)
E: packagingmono: dir-or-file-in-opt opt/PackagingMono/
E: packagingmono: dir-or-file-in-opt opt/PackagingMono/.gitignore
W: packagingmono: package-contains-vcs-control-file opt/PackagingMono/.gitignore
E: packagingmono: dir-or-file-in-opt opt/PackagingMono/PackagingMono.exe
E: packagingmono: dir-or-file-in-opt opt/PackagingMono/PackagingMono.exe.config

There are a couple of errors reported by Lintian. But as I mentioned I run a simple build process with a minimalistic package. The created package will work in spite of these errors.
Let’s test the installation and run the program:

mike@mike-machine $ sudo dpkg --install ./Package/PackagingMono_1.0.1216.5.deb 
[sudo] password for mike: 
Выбор ранее не выбранного пакета packagingmono.
(Чтение базы данных … на данный момент установлено 168420 файлов и каталогов.)
Preparing to unpack …/PackagingMono_1.0.1216.5.deb ...
Unpacking packagingmono (1.0.1216.5) ...
Настраивается пакет packagingmono (1.0.1216.5) …

mike@mike-machine $ /opt/PackagingMono/PackagingMono.exe 
Hello World!