SCons and C#

It would be great to roll this into a generic CLI builder that can take sources from any supported CLI language and compile them to EXE or DLL.

Mono

Here is a simple mcs builder. It takes one or more C# files and creates and EXE or a DLL from them. The user variables that effect the build are:

Example Usage

   1 env.Tool('mcs', toolpath = [''])
   2 env.CLILibrary('Foo.dll', 'Foo.dll')
   3 env.CLIProgram('Bar.exe', 'Bar.exe')

Builder

   1 import os.path
   2 import SCons.Builder
   3 import SCons.Node.FS
   4 import SCons.Util
   5 
   6 csccom = "$CSC $CSCFLAGS -out:${TARGET.abspath} $SOURCES"
   7 csclibcom = "$CSC -t:library $CSCLIBFLAGS $_CSCLIBPATH $_CSCLIBS -out:${TARGET.abspath} $SOURCES"
   8 
   9 
  10 McsBuilder = SCons.Builder.Builder(action = '$CSCCOM',
  11                                    source_factory = SCons.Node.FS.default_fs.Entry,
  12                                    suffix = '.exe')
  13 
  14 McsLibBuilder = SCons.Builder.Builder(action = '$CSCLIBCOM',
  15                                    source_factory = SCons.Node.FS.default_fs.Entry,
  16                                    suffix = '.dll')
  17 
  18 def generate(env):
  19     env['BUILDERS']['CLIProgram'] = McsBuilder
  20     env['BUILDERS']['CLILibrary'] = McsLibBuilder
  21 
  22     env['CSC']        = 'mcs'
  23     env['_CSCLIBS']    = "${_stripixes('-r:', CILLIBS, '', '-r', '', __env__)}"
  24     env['_CSCLIBPATH'] = "${_stripixes('-lib:', CILLIBPATH, '', '-r', '', __env__)}"
  25     env['CSCFLAGS']   = SCons.Util.CLVar('')
  26     env['CSCCOM']     = SCons.Action.Action(csccom)
  27     env['CSCLIBCOM']  = SCons.Action.Action(csclibcom)
  28 
  29 def exists(env):
  30     return internal_zip or env.Detect('mcs')

Microsoft C# compiler

Example Usage (Library)

   1 refpaths = []
   2 
   3 refs = Split("""
   4   System
   5   System.Data
   6   System.Xml
   7   """)
   8 
   9 sources = Split("""
  10   DataHelper.cs
  11   Keyfile.snk
  12         """)
  13 
  14 r = env.CLIRefs(refpaths, refs)
  15 
  16 prog = env.CLILibrary('MyAssembly.Common', sources, ASSEMBLYREFS=r)
  17 # use the following call to allow programs built after this library to find it
  18 # without having to add to the refpaths (see next example)
  19 env.AddToRefPaths(prog)

Example Usage (Program)

   1 refpaths = Split("""
   2   #/thirdparty/EncryptionLib
   3   """)
   4 
   5 # note we don't have to add MyAssembly.Common's location to refpaths
   6 # it will be stored with the call to AddToRefPaths() in the above example
   7 refs = Split("""
   8   MyAssembly.Common
   9   System
  10   System.Data
  11   """)
  12 
  13 sources = Split("""
  14   Main.cs
  15   gui/App.cs
  16   gui/MyForm.cs
  17   Keyfile.snk
  18   """)
  19 
  20 resx = Split("""
  21   gui/App.resx
  22   gui/MyForm.AddServerModelForm.resx
  23   """)
  24 sources.append([env.CLIRES(r, NAMESPACE='MyCompany') for r in resx])
  25 
  26 r = env.CLIRefs(refpaths, refs)
  27 prog = env.CLIProgram('myapp', sources, ASSEMBLYREFS=r, WINEXE=1)

A Small, Complete Example

   1 import os
   2 
   3 env_vars = {}
   4 for ev in ['PATH', 'LIB', 'SYSTEMROOT', 'PYTHONPATH']:
   5   env_vars[ev] = os.environ[ev]
   6 
   7 env = Environment(
   8   platform='win32',
   9   tools=['mscs', 'msvs'],
  10   toolpath = ['.'],
  11   ENV=env_vars,
  12   MSVS_IGNORE_IDE_PATHS=1
  13   )
  14 
  15 mod = env.CLIModule('mymod', 'MyMod.cs')
  16 
  17 refs = ['System', 'System.Reflection', 'System.Runtime.CompilerServices', 'System.Runtime.InteropServices']
  18 pathrefs = env.CLIRefs(refs)
  19 
  20 mod2 = env.CLIModule('common', 'AsmInfo.cs', ASSEMBLYREFS=pathrefs) #, NETMODULES=mod)
  21 
  22 # Note that CLILink actually uses the VS 2005 C++ linker, since it can handle linking .netmodules
  23 asm = env.CLILink('Common', [mod, mod2])
  24 env.AddToRefPaths(asm)
  25 
  26 # WINEXE=1 needed if this is a windows app, rather than a console app
  27 prog = env.CLIProgram('MyApp', 'MyApp.cs', ASSEMBLYREFS=asm, VERSION="1.0.1.0")
  28 
  29 # Resolve location of Common assembly, this was registered with AddToRefPaths, above.
  30 # added_paths included simply to show how to add assembly paths to the lookup besides
  31 # the ones in the PATH environment variable.  Leave this argument out if there are none.
  32 added_paths = ['#/path']
  33 rr = env.CLIRefs(['Common'], added_paths)
  34 
  35 # VERSION can also be passed by tuple, rather than string.
  36 # "asm" variable could have been passed directly into ASSEMBLYREFS if we wanted.
  37 asm2 = env.CLILibrary('MyAsm', 'MyClass.cs', ASSEMBLYREFS=rr, VERSION=(1,0,1,0))
  38 
  39 # Create a publisher policy to redirect anything with major minor 
  40 # version of assembly to the MyAsm assembly above.
  41 policy = env.PublisherPolicy(asm2)

Builder

   1 import os.path
   2 import string
   3 import SCons.Builder
   4 import SCons.Node.FS
   5 import SCons.Util
   6 from SCons.Node.Python import Value
   7 
   8 # needed for adding methods to environment
   9 from SCons.Script.SConscript import SConsEnvironment
  10 
  11 # parses env['VERSION'] for major, minor, build, and revision
  12 def parseVersion(env):
  13   if type(env['VERSION']) is tuple or type(env['VERSION']) is list:
  14     major, minor, build, revision = env['VERSION']
  15   elif type(env['VERSION']) is str:
  16     major, minor, build, revision = string.split(env['VERSION'], '.')
  17     major = int(major)
  18     minor = int(minor)
  19     build = int(build)
  20     revision = int(revision)
  21   return (major, minor, build, revision)
  22 
  23 def getVersionAsmDirective(major, minor, build, revision):
  24   return '[assembly: AssemblyVersion("%d.%d.%d.%d")]' % (major, minor, build, revision)
  25 
  26 def generateVersionId(env, target, source):
  27   out = open(target[0].path, 'w')
  28   out.write('using System;using System.Reflection;using System.Runtime.CompilerServices;using System.Runtime.InteropServices;\n')
  29   out.write(source[0].get_contents())
  30   out.close()
  31 
  32 # used so that we can capture the return value of an executed command
  33 def subprocess(cmdline):
  34   import subprocess
  35   startupinfo = subprocess.STARTUPINFO()
  36   startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  37   proc = subprocess.Popen(cmdline, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
  38     stderr=subprocess.PIPE, startupinfo=startupinfo, shell=False)
  39   data, err = proc.communicate()
  40   return proc.wait(), data, err
  41 
  42 # this method assumes that source list corresponds to [0]=version, [1]=assembly base name, [2]=assembly file node
  43 def generatePublisherPolicyConfig(env, target, source):
  44   # call strong name tool against compiled assembly and parse output for public token
  45   outputFolder = os.path.split(target[0].tpath)[0]
  46   pubpolicy = os.path.join(outputFolder, source[2].name)
  47   rv, data, err = subprocess('sn -T ' + pubpolicy)
  48   import re
  49   tok_re = re.compile(r"([a-z0-9]{16})[\r\n ]{0,3}$")
  50   match = tok_re.search(data)
  51   tok = match.group(1)
  52 
  53   # calculate version range to redirect from
  54   version = source[0].value
  55   oldVersionStartRange = '%s.%s.0.0' % (version[0], version[1])
  56   newVersion = '%s.%s.%s.%s' % (version[0], version[1], version[2], version[3])
  57   build = int(version[2])
  58   rev = int(version[3])
  59 
  60   # on build 0 and rev 0 or 1, no range is needed. otherwise calculate range    
  61   if (build == 0 and (rev == 0 or rev == 1)):
  62     oldVersionRange = oldVersionStartRange
  63   else:
  64     if rev - 1 < 0:
  65       endRevisionRange = '99'
  66       endBuildRange = str(build-1)
  67     else:
  68       endRevisionRange = str(rev - 1)
  69       endBuildRange = str(build)
  70     oldVersionEndRange = '%s.%s.%s.%s' % (version[0], version[1], endBuildRange, endRevisionRange)
  71     oldVersionRange = '%s-%s' % (oldVersionStartRange, oldVersionEndRange)
  72 
  73   # write .net config xml out to file
  74   out = open(target[0].path, 'w')
  75   out.write('''\
  76 <configuration><runtime><assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
  77   <dependentAssembly>
  78     <assemblyIdentity name="%s" publicKeyToken="%s"/>
  79     <bindingRedirect oldVersion="%s" newVersion="%s"/>
  80   </dependentAssembly>
  81 </assemblyBinding></runtime></configuration>
  82 ''' % (source[1].value, tok, oldVersionRange, newVersion))
  83   out.close()
  84 
  85 # search for key file
  86 def getKeyFile(node, sources):
  87   for file in node.children():
  88     if file.name.endswith('.snk'):
  89       sources.append(file)
  90       return
  91 
  92   # if not found look in included netmodules (first found is used)
  93   for file in node.children():
  94     if file.name.endswith('.netmodule'):
  95       for file2 in file.children():
  96         if file2.name.endswith('.snk'):
  97           sources.append(file2)
  98           return
  99 
 100 # creates the publisher policy dll, mapping the major.minor.0.0 calls to the 
 101 # major, minor, build, and revision passed in through the dictionary VERSION key
 102 def PublisherPolicy(env, target, **kw):
 103   sources = []
 104   # get version and generate .config file
 105   version = parseVersion(kw)
 106   asm = os.path.splitext(target[0].name)[0]
 107   configName = 'policy.%d.%d.%s.%s' % (version[0], version[1], asm, 'config')
 108   targ = 'policy.%d.%d.%s' % (version[0], version[1], target[0].name)
 109   config = env.Command(configName, [Value(version), Value(asm), target[0]], generatePublisherPolicyConfig)
 110   sources.append(config[0])
 111 
 112   # find .snk key
 113   getKeyFile(target[0], sources)
 114 
 115   return env.CLIAsmLink(targ, sources, **kw)
 116 
 117 def CLIRefs(env, refs, paths = [], **kw):
 118   listRefs = []
 119   normpaths = [env.Dir(p).abspath for p in paths]
 120   normpaths += env['CLIREFPATHS']
 121 
 122   for ref in refs:
 123     if not ref.endswith(env['SHLIBSUFFIX']):
 124       ref += env['SHLIBSUFFIX']
 125     if not ref.startswith(env['SHLIBPREFIX']):
 126       ref = env['SHLIBPREFIX'] + ref
 127     pathref = detectRef(ref, normpaths, env)
 128     if pathref:
 129       listRefs.append(pathref)
 130 
 131   return listRefs
 132 
 133 def CLIMods(env, refs, paths = [], **kw):
 134   listMods = []
 135   normpaths = [env.Dir(p).abspath for p in paths]
 136   normpaths += env['CLIMODPATHS']
 137 
 138   for ref in refs:
 139     if not ref.endswith(env['CLIMODSUFFIX']):
 140       ref += env['CLIMODSUFFIX']
 141     pathref = detectRef(ref, normpaths, env)
 142     if pathref:
 143       listMods.append(pathref)
 144 
 145   return listMods
 146 
 147 # look for existance of file (ref) at one of the paths
 148 def detectRef(ref, paths, env):
 149   for path in paths:
 150     if path.endswith(ref):
 151       return path
 152     pathref = os.path.join(path, ref)
 153     if os.path.isfile(pathref):
 154       return pathref
 155 
 156   return ''
 157 
 158 # the file name is included in path reference because otherwise checks for that output file
 159 # by CLIRefs/CLIMods would fail until after it has been built.  Since SCons makes a pass
 160 # before building anything, that file won't be there.  Only after the second pass will it be built
 161 def AddToRefPaths(env, files, **kw):
 162   ref = env.FindIxes(files, 'SHLIBPREFIX', 'SHLIBSUFFIX').abspath
 163   env['CLIREFPATHS'] = [ref] + env['CLIREFPATHS']
 164   return 0
 165 
 166 def AddToModPaths(env, files, **kw):
 167   mod = env.FindIxes(files, 'CLIMODPREFIX', 'CLIMODSUFFIX').abspath
 168   env['CLIMODPATHS'] = [mod] + env['CLIMODPATHS']
 169   return 0
 170 
 171 def cscFlags(target, source, env, for_signature):
 172   listCmd = []
 173   if (env.has_key('WINEXE')):
 174     if (env['WINEXE'] == 1):
 175       listCmd.append('-t:winexe')
 176   return listCmd
 177 
 178 def cscSources(target, source, env, for_signature):
 179   listCmd = []
 180 
 181   for s in source:
 182     if (str(s).endswith('.cs')):  # do this first since most will be source files
 183       listCmd.append(s)
 184     elif (str(s).endswith('.resources')):
 185       listCmd.append('-resource:%s' % s.get_string(for_signature))
 186     elif (str(s).endswith('.snk')):
 187       listCmd.append('-keyfile:%s' % s.get_string(for_signature))
 188     else:
 189       # just treat this as a generic unidentified source file
 190       listCmd.append(s)
 191 
 192   return listCmd
 193 
 194 def cscRefs(target, source, env, for_signature):
 195   listCmd = []
 196 
 197   if (env.has_key('ASSEMBLYREFS')):
 198     refs = SCons.Util.flatten(env['ASSEMBLYREFS'])
 199     for ref in refs:
 200       if SCons.Util.is_String(ref):
 201         listCmd.append('-reference:%s' % ref)
 202       else:
 203         listCmd.append('-reference:%s' % ref.abspath)
 204 
 205   return listCmd
 206 
 207 def cscMods(target, source, env, for_signature):
 208   listCmd = []
 209 
 210   if (env.has_key('NETMODULES')):
 211     mods = SCons.Util.flatten(env['NETMODULES'])
 212     for mod in mods:
 213       listCmd.append('-addmodule:%s' % mod)
 214 
 215   return listCmd
 216 
 217 # TODO: this currently does not allow sources to be embedded (-embed flag)
 218 def alLinkSources(target, source, env, for_signature):
 219   listCmd = []
 220 
 221   for s in source:
 222     if (str(s).endswith('.snk')):
 223       listCmd.append('-keyfile:%s' % s.get_string(for_signature))
 224     else:
 225       # just treat this as a generic unidentified source file
 226       listCmd.append('-link:%s' % s.get_string(for_signature))
 227 
 228   if env.has_key('VERSION'):
 229     version = parseVersion(env)
 230     listCmd.append('-version:%d.%d.%d.%d' % version)
 231 
 232   return listCmd
 233 
 234 def add_version(target, source, env):
 235   if env.has_key('VERSION'):
 236     if SCons.Util.is_String(target[0]):
 237       versionfile = target[0] + '_VersionInfo.cs'
 238     else:
 239       versionfile = target[0].name + '_VersionInfo.cs'
 240     source.append(env.Command(versionfile, [Value(getVersionAsmDirective(*parseVersion(env)))], generateVersionId))
 241   return (target, source)
 242 
 243 MsCliBuilder = SCons.