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:
- CSC: the name of the compiler (in this case mcs)
- CSCFLAGS: extra compiler flags
- CILLIBS: libraries to link against
- CILLIBPATH: library paths
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.