Python Script to Build, Test and Pack .NET Projects

At work we use Jenkins as our continuous integration server. Our implementation at work has to integrate with ClearCase and does a few tasks before actually doing the build. This is pretty typical. Our setup then calls an external script to build. We are not using some of the features that can be configured for Hudson. This may not be as stream-lined as it can be but it has some benefits and I have to work within these constraints.

Since our build machine already has Python installed, I thought I would see if I can put together a reusable build script. I played around a bit and landed upon the following to call build (MSBuild), execute tests (MSTest) and create Nuget packages. I thought I would share this with you.

# Generic build script that builds, tests, and creates nuget packages.
#
# INSTRUCTIONS:
#    Update the following project paths:
#        proj        Path to the project file (.csproj)
#        test        Path to the test project (.csproj)
#        nuspec        Path to the package definition for NuGet.
#
#        delete any of the lines if not applicable
#
#
#    Update the paths to the build tools:
#        msbuild        Path to msbuild
#        test        Path to mstest.exe (requires Visual Studio) (optional – set to None)
#        nuget        Path to nuget.exe (requires NuGet command line tool) (optional - set to None)
#        trx2html    Path to trx2html.exe (http://trx2html.codeplex.com/) (optional - set to None)
#
#
# USAGE:
#
#     proj = r'path to project (.csproj)'
#     test = r'path to project containing test (.csproj)'
#     nuspec = r'path to nuspec definition (.nuspec)'
#
#
#     msbuild = r'C:WindowsMicrosoft.NETFrameworkv4.0.30319MSBuild.exe'
#     mstest = r'C:Program Files (x86)Microsoft Visual Studio 10.0Common7IDEMSTest.exe'
#     nuget = r'C:BuildToolsnuget2199eada12cenuget.exe'
#     trx2html = r'C:BuildToolstrx2html.6trx2html.exe'
#
#     bld = MsBuilder(msbuild, mstest, nuget, trx2html)
#     bld.run(proj, test, nuspec)
#

import os, shlex, subprocess, re, datetime

class MsBuilder:
    def __init__(self, msbuild=None, mstest=None, nuget=None, trx2html=None):
        # The following dictionary holds the location of the various
        #    msbuild.exe paths for the .net framework versions
        if msbuild==None:
            self.msbuild = r'C:WindowsMicrosoft.NETFrameworkv4.0.30319MSBuild.exe'
        else:
            self.msbuild = msbuild

        # Path to mstest (this requires vs2010 to be installed
        if mstest==None:
            self.mstest = r'C:Program Files (x86)Microsoft Visual Studio 10.0Common7IDEMSTest.exe'
        else:
            self.mstest = mstest

        # Path to nuget packager
        if nuget==None:
            self.nuget = r'C:BuildToolsnuget2199eada12cenuget.exe'
        else:
            self.nuget = nuget

        # Path to trx2html transformation tool
        if trx2html==None:
            self.trx2html = r'C:BuildToolstrx2html.6trx2html.exe'
        else:
            self.trx2html = trx2html

    def build(self, projPath):
        # Ensure msbuild exists
        if not os.path.isfile(self.msbuild):
            raise Exception('MsBuild.exe not found. path=' + self.msbuild)

        arg1 = '/t:Rebuild'
        arg2 = '/p:Configuration=Release'
        p = subprocess.call([self.msbuild, projPath, arg1, arg2])
        if p==1: return False    # exit early

        return True

    def test(self, testProject):
        if not os.path.isfile(self.msbuild):
            raise Exception('MsBuild.exe not found. path=' + self.msbuild)
        if not os.path.isfile(self.mstest):
            raise Exception('MsTest.exe not found. path=' + self.mstest)

        # build the test project
        arg1 = '/t:Rebuild'
        arg2 = '/p:Configuration=Release'
        p = subprocess.call([self.msbuild, testProject, arg1, arg2])

        # find the test dll
        f = open(testProject)
        xml = f.read()
        f.close()
        match = re.search(r'<AssemblyName>(.*)</AssemblyName>', xml)
        if not match:
            print 'Could not find "AssemblyName" in test project file.'
            return False

        outputFolder = os.path.dirname(testProject) + '\bin\Release\'
        dll =  outputFolder + match.groups()[0] + '.dll'
        resultFile = outputFolder + 'testResults.trx'
        if os.path.isfile(resultFile):
            os.remove(resultFile)

        # execute the tests in the container
        arg1 = '/testcontainer:' + dll
        arg2 = '/resultsfile:' + resultFile
        p = subprocess.call([self.mstest, arg1, arg2])

        # convert the results file
        if os.path.isfile(self.trx2html):
            subprocess.call([self.trx2html, resultFile])
        else:
            print 'TRX to HTML converter not found. path=' + self.trx2html

        if p==1: return False # exit early

        return True

    def pack(self, packageSpec, version = '0.0.0.0'):
        if not os.path.isfile(self.nuget):
            raise Exception('Nuget.exe not found. path=' + self.nuget)

        outputFolder = os.path.dirname(packageSpec) + '\artifacts\'
        if not os.path.exists(outputFolder):
            os.makedirs(outputFolder)

        p = subprocess.call([self.nuget, 'pack', packageSpec, '-Version', version, '-Symbols', '-o', outputFolder])

        if p==1: return False #exit early

        return True


    def validate(self, projectPath):
        packFile = os.path.dirname(projectPath) + '\packages.config'
        if os.path.isfile(packFile):
            f = open(packFile)
            xml = f.read()
            f.close()
            print xml
            match = re.search(r'version="0.0.0.0"', xml)
            if match:
                # Found a non-versioned package being used by this project
                return False
        else:
            print 'No "packages.config" file was found. path=' + packFile

        return True

    def run(self, proj=None, test=None, nuspec=None):
        summary = '';

        # File header
        start = datetime.datetime.now()
        print 'n'*5
        summary += self.log('STARTED BUILD - ' + start.strftime("%Y-%m-%d %H:%M:%S"))

        # Build the project
        if proj is not None:
            buildOk = self.build(proj)
            if not buildOk:
                self.log('BUILD: FAILED', start)
                sys.exit(100)
            summary += self.log('BUILD: SUCCEEDED', start)
        else:
            summary += self.log('BUILD: NOT SPECIFIED')

        # Build the tests and run them
        if test is not None:
            testOk = self.test(test)
            if not testOk:
                print self.log('TESTS: FAILED', start)
                sys.exit(100)
            summary += self.log('TESTS: PASSED', start)
        else:
            summary += self.log('TESTS: NOT SPECIFIED')

        # Package up the artifacts
        if nuspec is not None:
            packOk = self.pack(nuspec, '0.0.0.0')
            if not packOk:
                print self.log('NUGET PACK: FAILED', start)
                sys.exit(100)
            summary += self.log('NUGET PACK: SUCCEEDED', start)
        else:
            summary += self.log('NUGET PACK: NOT SPECIFIED')

        # Validate dependencies
        if not self.validate(proj):
            print self.log('DEPENDENCIES: NOT VALIDATED - DETECTED UNVERSIONED DEPENDENCY', start)
            sys.exit(100)
        summary += self.log('DEPENDENCIES: VALIDATED', start)

        # Build footer
        stop = datetime.datetime.now()
        diff = stop - start
        summary += self.log('FINISHED BUILD', start)

        # Build summary
        print 'nn' + '-'*80
        print summary
        print '-'*80

    def log(self, message, start=None):
        timestamp = ''
        numsecs = ''
        if start is not None:
            split = datetime.datetime.now()
            diff = split - start
            timestamp = split.strftime("%Y-%m-%d %H:%M:%S") + 't'
            numsecs = ' (' + str(diff.seconds) + ' seconds)'
        msg = timestamp + message + numsecs + 'nn'
        print '='*10 + '> ' + msg
        return msg

You can also find this script on github. I am not a Python guru, so if you see opportunities for improvement provide comments on the github page.

Leave a Reply

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

*