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.