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.
Russel Winder has started a Mercurial repository of a SCons C# tool based on the code from this page. See https://bitbucket.org/russel/scons_csharp
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:
- CSC: the name of the compiler (in this case mcs)
- CSCFLAGS: extra compiler flags
- CILLIBS: libraries to link against
- CILLIBPATH: library paths
Example Usage
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 SCons.Builder
3 import SCons.Node.FS
4 import SCons.Util
5 from SCons.Node.Python import Value
6
7 # needed for adding methods to environment
8 from SCons.Script.SConscript import SConsEnvironment
9
10 # parses env['VERSION'] for major, minor, build, and revision
11 def parseVersion(env):
12 if type(env['VERSION']) is tuple or type(env['VERSION']) is list:
13 major, minor, build, revision = env['VERSION']
14 elif type(env['VERSION']) is str:
15 major, minor, build, revision = env['VERSION'].split('.')
16 major = int(major)
17 minor = int(minor)
18 build = int(build)
19 revision = int(revision)
20 return (major, minor, build, revision)
21
22 def getVersionAsmDirective(major, minor, build, revision):
23 return '[assembly: AssemblyVersion("%d.%d.%d.%d")]' % (major, minor, build, revision)
24
25 def generateVersionId(env, target, source):
26 out = open(target[0].path, 'w')
27 out.write('using System;using System.Reflection;using System.Runtime.CompilerServices;using System.Runtime.InteropServices;\n')
28 out.write(source[0].get_contents())
29 out.close()
30
31 # used so that we can capture the return value of an executed command
32 def subprocess(cmdline):
33 import subprocess
34 startupinfo = subprocess.STARTUPINFO()
35 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
36 proc = subprocess.Popen(cmdline, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
37 stderr=subprocess.PIPE, startupinfo=startupinfo, shell=False)
38 data, err = proc.communicate()
39 return proc.wait(), data, err
40
41 # this method assumes that source list corresponds to [0]=version, [1]=assembly base name, [2]=assembly file node
42 def generatePublisherPolicyConfig(env, target, source):
43 # call strong name tool against compiled assembly and parse output for public token
44 outputFolder = os.path.split(target[0].tpath)[0]
45 pubpolicy = os.path.join(outputFolder, source[2].name)
46 rv, data, err = subprocess('sn -T ' + pubpolicy)
47 import re
48 tok_re = re.compile(r"([a-z0-9]{16})[\r\n ]{0,3}$")
49 match = tok_re.search(data)
50 tok = match.group(1)
51
52 # calculate version range to redirect from
53 version = source[0].value
54 oldVersionStartRange = '%s.%s.0.0' % (version[0], version[1])
55 newVersion = '%s.%s.%s.%s' % (version[0], version[1], version[2], version[3])
56 build = int(version[2])
57 rev = int(version[3])
58
59 # on build 0 and rev 0 or 1, no range is needed. otherwise calculate range
60 if build == 0 and (rev == 0 or rev == 1):
61 oldVersionRange = oldVersionStartRange
62 else:
63 if rev - 1 < 0:
64 endRevisionRange = '99'
65 endBuildRange = str(build-1)
66 else:
67 endRevisionRange = str(rev - 1)
68 endBuildRange = str(build)
69 oldVersionEndRange = '%s.%s.%s.%s' % (version[0], version[1], endBuildRange, endRevisionRange)
70 oldVersionRange = '%s-%s' % (oldVersionStartRange, oldVersionEndRange)
71
72 # write .net config xml out to file
73 out = open(target[0].path, 'w')
74 out.write('''\
75 <configuration><runtime><assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
76 <dependentAssembly>
77 <assemblyIdentity name="%s" publicKeyToken="%s"/>
78 <bindingRedirect oldVersion="%s" newVersion="%s"/>
79 </dependentAssembly>
80 </assemblyBinding></runtime></configuration>
81 ''' % (source[1].value, tok, oldVersionRange, newVersion))
82 out.close()
83
84 # search for key file
85 def getKeyFile(node, sources):
86 for file in node.children():
87 if file.name.endswith('.snk'):
88 sources.append(file)
89 return
90
91 # if not found look in included netmodules (first found is used)
92 for file in node.children():
93 if file.name.endswith('.netmodule'):
94 for file2 in file.children():
95 if file2.name.endswith('.snk'):
96 sources.append(file2)
97 return
98
99 # creates the publisher policy dll, mapping the major.minor.0.0 calls to the
100 # major, minor, build, and revision passed in through the dictionary VERSION key
101 def PublisherPolicy(env, target, **kw):
102 sources = []
103 # get version and generate .config file
104 version = parseVersion(kw)
105 asm = os.path.splitext(target[0].name)[0]
106 configName = 'policy.%d.%d.%s.%s' % (version[0], version[1], asm, 'config')
107 targ = 'policy.%d.%d.%s' % (version[0], version[1], target[0].name)
108 config = env.Command(configName, [Value(version), Value(asm), target[0]], generatePublisherPolicyConfig)
109 sources.append(config[0])
110
111 # find .snk key
112 getKeyFile(target[0], sources)
113
114 return env.CLIAsmLink(targ, sources, **kw)
115
116 def CLIRefs(env, refs, paths = [], **kw):
117 listRefs = []
118 normpaths = [env.Dir(p).abspath for p in paths]
119 normpaths += env['CLIREFPATHS']
120
121 for ref in refs:
122 if not ref.endswith(env['SHLIBSUFFIX']):
123 ref += env['SHLIBSUFFIX']
124 if not ref.startswith(env['SHLIBPREFIX']):
125 ref = env['SHLIBPREFIX'] + ref
126 pathref = detectRef(ref, normpaths, env)
127 if pathref:
128 listRefs.append(pathref)
129
130 return listRefs
131
132 def CLIMods(env, refs, paths = [], **kw):
133 listMods = []
134 normpaths = [env.Dir(p).abspath for p in paths]
135 normpaths += env['CLIMODPATHS']
136
137 for ref in refs:
138 if not ref.endswith(env['CLIMODSUFFIX']):
139 ref += env['CLIMODSUFFIX']
140 pathref = detectRef(ref, normpaths, env)
141 if pathref:
142 listMods.append(pathref)
143
144 return listMods
145
146 # look for existance of file (ref) at one of the paths
147 def detectRef(ref, paths, env):
148 for path in paths:
149 if path.endswith(ref):
150 return path
151 pathref = os.path.join(path, ref)
152 if os.path.isfile(pathref):
153 return pathref
154
155 return ''
156
157 # the file name is included in path reference because otherwise checks for that output file
158 # by CLIRefs/CLIMods would fail until after it has been built. Since SCons makes a pass
159 # before building anything, that file won't be there. Only after the second pass will it be built
160 def AddToRefPaths(env, files, **kw):
161 ref = env.FindIxes(files, 'SHLIBPREFIX', 'SHLIBSUFFIX').abspath
162 env['CLIREFPATHS'] = [ref] + env['CLIREFPATHS']
163 return 0
164
165 def AddToModPaths(env, files, **kw):
166 mod = env.FindIxes(files, 'CLIMODPREFIX', 'CLIMODSUFFIX').abspath
167 env['CLIMODPATHS'] = [mod] + env['CLIMODPATHS']
168 return 0
169
170 def cscFlags(target, source, env, for_signature):
171 listCmd = []
172 if 'WINEXE' in env:
173 if env['WINEXE'] == 1:
174 listCmd.append('-t:winexe')
175 return listCmd
176
177 def cscSources(target, source, env, for_signature):
178 listCmd = []
179
180 for s in source:
181 if str(s).endswith('.cs'): # do this first since most will be source files
182 listCmd.append(s)
183 elif str(s).endswith('.resources'):
184 listCmd.append('-resource:%s' % s.get_string(for_signature))
185 elif str(s).endswith('.snk'):
186 listCmd.append('-keyfile:%s' % s.get_string(for_signature))
187 else:
188 # just treat this as a generic unidentified source file
189 listCmd.append(s)
190
191 return listCmd
192
193 def cscRefs(target, source, env, for_signature):
194 listCmd = []
195
196 if 'ASSEMBLYREFS' in env:
197 refs = SCons.Util.flatten(env['ASSEMBLYREFS'])
198 for ref in refs:
199 if SCons.Util.is_String(ref):
200 listCmd.append('-reference:%s' % ref)
201 else:
202 listCmd.append('-reference:%s' % ref.abspath)
203
204 return listCmd
205
206 def cscMods(target, source, env, for_signature):
207 listCmd = []
208
209 if 'NETMODULES' in env:
210 mods = SCons.Util.flatten(env['NETMODULES'])
211 for mod in mods:
212 listCmd.append('-addmodule:%s' % mod)
213
214 return listCmd
215
216 # TODO: this currently does not allow sources to be embedded (-embed flag)
217 def alLinkSources(target, source, env, for_signature):
218 listCmd = []
219
220 for s in source:
221 if str(s).endswith('.snk'):
222 listCmd.append('-keyfile:%s' % s.get_string(for_signature))
223 else:
224 # just treat this as a generic unidentified source file
225 listCmd.append('-link:%s' % s.get_string(for_signature))
226
227 if 'VERSION' in env:
228 version = parseVersion(env)
229 listCmd.append('-version:%d.%d.%d.%d' % version)
230
231 return listCmd
232
233 def add_version(target, source, env):
234 if 'VERSION' in env:
235 if SCons.Util.is_String(target[0]):
236 versionfile = target[0] + '_VersionInfo.cs'
237 else:
238 versionfile = target[0].name + '_VersionInfo.cs'
239 source.append(env.Command(versionfile, [Value(getVersionAsmDirective(*parseVersion(env)))], generateVersionId))
240 return (target, source)
241
242 MsCliBuilder = SCons.Builder.Builder(action = '$CSCCOM',
243 source_factory = SCons.Node.FS.default_fs.Entry,
244 emitter = add_version,
245 suffix = '.exe')
246
247 # this check is needed because .NET assemblies like to have '.' in the name.
248 # scons interprets that as an extension and doesn't append the suffix as a result
249 def lib_emitter(target, source, env):
250 newtargets = []
251 for tnode in target:
252 t = tnode.name
253 if not t.endswith(env['SHLIBSUFFIX']):
254 t += env['SHLIBSUFFIX']
255 newtargets.append(t)
256
257 return (newtargets, source)
258
259 def add_depends(target, source, env):
260 """Add dependency information before the build order is established"""
261
262 if 'NETMODULES' in env:
263 mods = SCons.Util.flatten(env['NETMODULES'])
264 for mod in mods:
265 # add as dependency if it is something we build
266 dir = env.File(mod).dir.srcdir
267 if dir is not None and dir is not type(None):
268 for t in target:
269 env.Depends(t, mod)
270
271 if 'ASSEMBLYREFS' in env:
272 refs = SCons.Util.flatten(env['ASSEMBLYREFS'])
273 for ref in refs:
274 # add as dependency if it is something we build
275 dir = env.File(ref).dir.srcdir
276 if dir is not None and dir is not type(None):
277 for t in target:
278 env.Depends(t, ref)
279
280 return (target, source)
281
282 MsCliLibBuilder = SCons.Builder.Builder(action = '$CSCLIBCOM',
283 source_factory = SCons.Node.FS.default_fs.Entry,
284 emitter = [lib_emitter, add_version, add_depends],
285 suffix = '$SHLIBSUFFIX')
286
287 MsCliModBuilder = SCons.Builder.Builder(action = '$CSCMODCOM',
288 source_factory = SCons.Node.FS.default_fs.Entry,
289 emitter = [add_version, add_depends],
290 suffix = '$CLIMODSUFFIX')
291
292 def module_deps(target, source, env):
293 for s in source:
294 dir = s.dir.srcdir
295 if dir is not None and dir is not type(None):
296 for t in target:
297 env.Depends(t,s)
298 return (target, source)
299
300 MsCliLinkBuilder = SCons.Builder.Builder(action = '$CLILINKCOM',
301 source_factory = SCons.Node.FS.default_fs.Entry,
302 emitter = [lib_emitter, add_version, module_deps], # don't know the best way yet to get module dependencies added
303 suffix = '.dll') #'$SHLIBSUFFIX')
304
305 # This probably needs some more work... it hasn't been used since
306 # finding the abilities of the VS 2005 C++ linker for .NET.
307 MsCliAsmLinkBuilder = SCons.Builder.Builder(action = '$CLIASMLINKCOM',
308 source_factory = SCons.Node.FS.default_fs.Entry,
309 suffix = '.dll')
310
311 typelib_prefix = 'Interop.'
312
313 def typelib_emitter(target, source, env):
314 newtargets = []
315 for tnode in target:
316 t = tnode.name
317 if not t.startswith(typelib_prefix):
318 t = typelib_prefix + t
319 newtargets.append(t)
320
321 return (newtargets, source)
322
323 def tlbimpFlags(target, source, env, for_signature):
324 listCmd = []
325
326 basename = os.path.splitext(target[0].name)[0]
327 # strip off typelib_prefix (such as 'Interop.') so it isn't in the namespace
328 if basename.startswith(typelib_prefix):
329 basename = basename[len(typelib_prefix):]
330 listCmd.append('-namespace:%s' % basename)
331
332 listCmd.append('-out:%s' % target[0].tpath)
333
334 for s in source:
335 if str(s).endswith('.snk'):
336 listCmd.append('-keyfile:%s' % s.get_string(for_signature))
337
338 return listCmd
339
340 MsCliTypeLibBuilder = SCons.Builder.Builder(action = '$TYPELIBIMPCOM',
341 source_factory = SCons.Node.FS.default_fs.Entry,
342 emitter = [typelib_emitter, add_depends],
343 suffix = '.dll')
344
345 res_action = SCons.Action.Action('$CLIRCCOM', '$CLIRCCOMSTR')
346
347 # prepend NAMESPACE if provided
348 def res_emitter(target, source, env):
349 if 'NAMESPACE' in env:
350 newtargets = []
351 for t in target:
352 tname = t.name
353
354 # this is a cheesy way to get rid of '.aspx' in .resx file names
355 idx = tname.find('.aspx.')
356 if idx >= 0:
357 tname = tname[:idx] + tname[idx+5:]
358
359 newtargets.append('%s.%s' % (env['NAMESPACE'], tname))
360 return (newtargets, source)
361 else:
362 return (targets, source)
363
364 res_builder = SCons.Builder.Builder(action=res_action,
365 emitter=res_emitter,
366 src_suffix='.resx',
367 suffix='.resources',
368 src_builder=[],
369 source_scanner=SCons.Tool.SourceFileScanner)
370
371 SCons.Tool.SourceFileScanner.add_scanner('.resx', SCons.Defaults.CScan)
372
373 def generate(env):
374 envpaths = env['ENV']['PATH']
375 env['CLIREFPATHS'] = envpaths.split(os.pathsep)
376 env['CLIMODPATHS'] = []
377 env['ASSEMBLYREFS'] = []
378 env['NETMODULES'] = []
379
380 env['BUILDERS']['CLIProgram'] = MsCliBuilder
381 env['BUILDERS']['CLIAssembly'] = MsCliLibBuilder
382 env['BUILDERS']['CLILibrary'] = MsCliLibBuilder
383 env['BUILDERS']['CLIModule'] = MsCliModBuilder
384 env['BUILDERS']['CLILink'] = MsCliLinkBuilder
385 env['BUILDERS']['CLIAsmLink'] = MsCliAsmLinkBuilder
386 env['BUILDERS']['CLITypeLib'] = MsCliTypeLibBuilder
387
388 env['CSC'] = 'csc'
389 env['_CSCLIBS'] = "${_stripixes('-r:', CILLIBS, '', '-r', '', __env__)}"
390 env['_CSCLIBPATH'] = "${_stripixes('-lib:', CILLIBPATH, '', '-r', '', __env__)}"
391 env['CSCFLAGS'] = SCons.Util.CLVar('-nologo -noconfig')
392 env['_CSCFLAGS'] = cscFlags
393 env['_CSC_SOURCES'] = cscSources
394 env['_CSC_REFS'] = cscRefs
395 env['_CSC_MODS'] = cscMods
396 env['CSCCOM'] = SCons.Action.Action('$CSC $CSCFLAGS $_CSCFLAGS -out:${TARGET.abspath} $_CSC_REFS $_CSC_MODS $_CSC_SOURCES', '$CSCCOMSTR')
397 env['CSCLIBCOM'] = SCons.Action.Action('$CSC -t:library $CSCFLAGS $_CSCFLAGS $_CSCLIBPATH $_CSCLIBS -out:${TARGET.abspath} $_CSC_REFS $_CSC_MODS $_CSC_SOURCES', '$CSCLIBCOMSTR')
398 env['CSCMODCOM'] = SCons.Action.Action('$CSC -t:module $CSCFLAGS $_CSCFLAGS -out:${TARGET.abspath} $_CSC_REFS $_CSC_MODS $_CSC_SOURCES', '$CSCMODCOMSTR')
399 env['CLIMODPREFIX'] = ''
400 env['CLIMODSUFFIX'] = '.netmodule'
401 env['CSSUFFIX'] = '.cs'
402
403 # this lets us link .netmodules together into a single assembly
404 env['CLILINK'] = 'link'
405 env['CLILINKFLAGS'] = SCons.Util.CLVar('-nologo -ltcg -dll -noentry')
406 env['CLILINKCOM'] = SCons.Action.Action('$CLILINK $CLILINKFLAGS -out:${TARGET.abspath} $SOURCES', '$CLILINKCOMSTR')
407
408 env['CLIASMLINK'] = 'al'
409 env['CLIASMLINKFLAGS'] = SCons.Util.CLVar('')
410 env['_ASMLINK_SOURCES'] = alLinkSources
411 env['CLIASMLINKCOM'] = SCons.Action.Action('$CLIASMLINK $CLIASMLINKFLAGS -out:${TARGET.abspath} $_ASMLINK_SOURCES', '$CLIASMLINKCOMSTR')
412
413 env['CLIRC'] = 'resgen'
414 env['CLIRCFLAGS'] = ''
415 env['CLIRCCOM'] = '$CLIRC $CLIRCFLAGS $SOURCES $TARGETS'
416 env['BUILDERS']['CLIRES'] = res_builder
417
418 env['TYPELIBIMP'] = 'tlbimp'
419 env['TYPELIBIMPFLAGS'] = SCons.Util.CLVar('-sysarray')
420 env['_TYPELIBIMPFLAGS'] = tlbimpFlags
421 env['TYPELIBIMPCOM'] = SCons.Action.Action('$TYPELIBIMP $SOURCES $TYPELIBIMPFLAGS $_TYPELIBIMPFLAGS', '$TYPELIBIMPCOMSTR')
422
423 SConsEnvironment.CLIRefs = CLIRefs
424 SConsEnvironment.CLIMods = CLIMods
425 SConsEnvironment.AddToRefPaths = AddToRefPaths
426 SConsEnvironment.AddToModPaths = AddToModPaths
427 SConsEnvironment.PublisherPolicy = PublisherPolicy
428
429 def exists(env):
430 return env.Detect('csc')
