Preamble

Disclaimer: I'm not a .NET programmer. The company I'm currently working for has large number of tools written in various languages (C/C++, C#, Perl, Java, etc.). We are in process of unifying builds of these various tools under Maven. This article describes how to add Maven build to your .NET project(s).

You may also consider using NPanday, which provides direct support for building .NET projects using Maven.

Prerequisites

This tutorial will not teach you basics of Maven. You should be aware of Maven lifecycle, standard directory layout, plugin (mojo) mechanism and profiles

Process

Build phases in details

Command line

You can build each individual sub-project by executing maven from the directory that contains child POM. The tutorial covers building multiple projects at once from parent level. The main difference in syntax is - when you build child you mustn't use "-rf ModuleName" command line option.

The following examples assume that you are building multiple projects from parent level

Build follows Maven build lifecycle so if you want your project to be packaged, use package goal, but if you want to use that as dependency you need install or deploy. You can chain goals as in the following example
Build is executed with this special syntax (note: subject to change)

mvn clean install

Profile build is enabled by default so you don't have to enforce it with "-P" switch. By default - the Release configuration is build. You can change it by defining foo.config property ether in settings.xml or simply by adding switch to command line

mvn clean install -Dfoo.config=Debug

Clean

Clean goal runs regular Maven clean-up (deletes PROJECT_ROOT/target directory and then executes MSBuild /t:Clean task. This and any other tasks that involve MSBuild utility are handled by exec-maven-plugin

Prepare

Optionally, before .NET build is run, Maven pulls any dependencies from the Maven repo and unpacks these to PROJECT_ROOT/lib directory. The .NET dependency artifacts are stored in the maven repo as ZIP archives. Each version is clearly marked with group ID, artifact ID and version number. For example REPO/mycompany/dept/foo/CalendarView/1.0/CalendarView-1.0.zip. ZIP contains all the artifacts (dll, exe, etc.) in the top directory of the archive. This task is handled by maven-dependency-plugin

Build

Build is handled by exec-maven-plugin which simply executes MSBuild.exe and provides runtime parameters. See follow-up configuration section for details

Install

Install/deploy creates ZIP archive of project artifacts and installs ZIP into local M2 repo or deploys ZIP to the SVN-backed repo. The process is done in two steps:

Publish

To publish artifacts to IIS you need to execute publish profile. Please note that publish will not run MSBuild's clean or build so before you execute publish you need run build profile with at least compile task, e.g. mvn compile -rf ReadCalendar

The main assumption of publishing a particular project is that MSBuild configuration for the project contains custom tasks t:/FOOPublish. Note that FOOPublish is a custom publish task for FOO project, you will have to define your own and you can call it anything as far as it is reflected correctly in the POM. Not all sub-projects under parent are publishable so profile section in the parent POM defines it's own module list (see Configuration section for details)

The command to publish. Note addition "-P publish" switch

mvn verify \-P publish \-rf CalendarView

Configuration

The complete configuration is split between the parent POM and at least 2 flavors of child POM depending if the child project is self-sufficient or specifies some dependencies

Please note that all plugin configurations described in the follow-up sections are placed into <profiles/> section of POM

<project>
...
    <profiles>
        <profile>
            <id>build</id>
            <build>
                <plugins>
                   <!-- Plugins configuration here -->
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

Dependencies

The dependencies are configured in child POM only. Since dependencies stored as ZIP files we cannot use common dependency mechanism provided by Maven. Instead we are using maven-dependency-plugin unpack artifact feature. Here's complete configuration from FOO/CalendarView project that defines dependency on FOO/ReadCalendar. Note that we have ability to specify a particular version

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>unpack</id>
            <phase>process-resources</phase>
            <goals>
                <goal>unpack</goal>
            </goals>
            <configuration>
                <artifactItems>
                    <artifactItem>
                        <groupId>mycompany.dept.foo</groupId>
                        <artifactId>ReadCalendar</artifactId>
                        <version>1.0-SNAPSHOT</version>
                        <type>zip</type>
                        <outputDirectory>${project.basedir}/lib</outputDirectory>
                        <overWrite>true</overWrite>
                    </artifactItem>
                </artifactItems>
            </configuration>
        </execution>
    </executions>
</plugin>

In this case - complete configuration is contained within child POM. The way it reads from top to bottom:

Cleanup and Build

This is "the meat" of the whole process. The configuration is split between the parent POM and child(ren) in such way that parent POM contains (in majority of cases) complete definition of profile(s) which keeps children POMs clean from repeatable code.
To suppress execution of profile in the parent you always need to execute parent-based build with --resume-from or -rf switch

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <configuration>
        <executable>${dotnet.path}/msbuild</executable>
    </configuration>
    <executions>
        <execution>
            <id>clean</id>
            <phase>clean</phase>
            <configuration>
                <arguments>
                    <argument>/p:Configuration=${foo.config}</argument>
                    <argument>/p:Platform=${foo.cpu}</argument>
                    <argument>/t:Clean</argument>
                </arguments>
            </configuration>
            <goals>
                <goal>exec</goal>
            </goals>
        </execution>
        <execution>
            <id>build</id>
            <phase>compile</phase>
            <configuration>
                <arguments>
                    <argument>/p:Configuration=${foo.config}</argument>
                    <argument>/p:Platform=${foo.cpu}</argument>
                    <argument>/t:Build</argument>
                </arguments>
            </configuration>
            <goals>
                <goal>exec</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Packaging (for Install or Deploy)

The packaging of artifacts requires two plugins and one assembly descriptor. The configuration is split between 2 files: parent POM and assembly descriptor.

Pre-packaging

Step 1 is to scan project for build artifacts and collecting these to lib directory. It's done by maven-antrun-plugin. Here's configuration

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.3</version>
    <configuration>
        <tasks>
            <copy todir="${project.build.directory}/dll-staging">
                <fileset dir="${basedir}">
                    <include name="**/*in/${foo.config}/*.dll" />
                    <include name="**/*in/${foo.config}/*.pdb" />
                    <include name="**/*in/${foo.config}/*.exe" />
                    <exclude name="**/Test*" />
                    <exclude name="**/test*" />
                </fileset>
                <flattenmapper />
            </copy>
        </tasks>
    </configuration>
    <executions>
        <execution>
            <id>copy</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
</plugin>

As you can see DLL, EXE and PDB files are collected to child's PROJECT_ROOT/target/dll-staging directory

Notice how this task is run at prepare-package phase

ZIPping up

This is done by maven-assembly-plugin

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <configuration>
        <descriptors>
            <descriptor>../assembly.xml</descriptor>
        </descriptors>
    </configuration>
    <executions>
        <execution>
            <id>zip</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<assembly>
    <formats>
        <format>zip</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <fileSets>
        <fileSet>
            <directory>${project.build.directory}/dll-staging</directory>
            <outputDirectory>/</outputDirectory>
            <includes>
                <include>*.dll</include>
                <include>*.exe</include>
                <include>*.pdb</include>
            </includes>
            <excludes>
                <exclude>Test*</exclude>
                <exclude>test*</exclude>
            </excludes>
        </fileSet>
    </fileSets>
</assembly>

Note that <include> and <exclude> configuration here is redundant since files are already filtered by pre-packaging step.
<includeBaseDirectory>false</includeBaseDirectory> is used to ensure that zipped files will go to the top directory of archive

References

There are few discussions on stackoverfow.com that I started during writing of this tutorial that you may find useful.

About author

I run Mea Cup O' Jo and DroidIn - Android app for LinkedIn blogs.