File: erskine3.py

package info (click to toggle)
yade 2019.01a-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye, buster, sid
  • size: 16,568 kB
  • sloc: cpp: 56,330; python: 30,148; ansic: 6,463; sh: 123; makefile: 56
file content (392 lines) | stat: -rw-r--r-- 14,008 bytes parent folder | download | duplicates (10)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#!/usr/bin/env python
# -*- Encoding: utf-8 -*-

"""
This program should really be run by the erskine3-apply.sh script

Erskine is Watt's colleague in the house of Mr. Nutting.

"""

import sys,pprint,string,os
from logging import *
from os.path import *
from re import *
from copy import deepcopy
import pprint

def warning2(msg): warning(currentPro+' '+msg)

bgroup=('(','{','[')
egroup=(')','}',']')
# variables that do not propagate to sub-builds
localVars=['TEMPLATE','SUBDIRS','HEADERS','SOURCES','FORMS','LEXSOURCES','YACCSOURCES','TARGET','DESTDIR','PROJECT']
# variables / function that are ignored if undefined (replaced by ''); for all other, a warning is printed (and are ignored anyway)
undefOK=['CXXPATH','CXX','CXXFLAGS','INCPATH','IDL_COMPILER']
# internal qmake variables that may be replace with the environment ones without warning
replaceOK=['YADECOMPILATIONPATH'] 
discardedStatements=['^isEmpty\s*\(\s*YADE_QMAKE_PATH\s*\).*$']
#discardedStatements=[]

targetLangType={'yade-lib-serialization-qt':'qt3','QtGUI':'qt3'}
# used to be yadeQt, but it was dropped...
targetEnv={'yade-lib-serialization-qt':'env','QtGUI':'env'}
# was: yade
defaultEnv='env'

currentPro='[none]'

allEnvs=[defaultEnv]
for t in targetEnv.keys():
	if not targetEnv[t] in allEnvs: allEnvs.append(targetEnv[t])
allSystemLibs=['glut','boost_date_time','boost_filesystem','boost_thread']


def splitgroup(s,delims=' '):
	"Split string at delims, but do not break parenthesized/bracketed/curly-bracketed groups"
	nest=0;ret=[];lastsplit=0
	for i in range(0,len(s)):
		if s[i] in bgroup: nest+=1
		elif s[i] in egroup: nest-=1
		elif (nest==0 and s[i] in delims):
			ret.append(s[lastsplit:i])
			lastsplit=i+1
	ret.append(s[lastsplit:len(s)])
	return ret

def parseProject(proFile,inheritedVariables={}):
	"""Returns dictionary of variables defined in this qmake project, replacing variables, that may propagate from parent project.
	No functions, only a few conditionals are expanded.
	
	An important 'parser' difference is that qmake obviously allows \\ continuation _after_ #.
	Since it is not nice at all, it doesn't work here. Also, it would be difficult to add."""

	vars=inheritedVariables

	# sanitize input: no comments, no \\ linebreaks (join lines), no empty lines, no leading/trailing whitespace
	qm=[]; l=''; nest=0
	for line in proFile:
		m=match(r'^\s*([^#]*)(#.*)?$',line[:-1])
		l+=m.group(1)
		l=l.rstrip()
		beginnest=nest
		if len(l)==0: continue
		for c in l:
			if c in bgroup:nest+=1
			elif c in egroup:nest-=1
		if l[-1]!='\\' and nest==0:
			qm.append(l)
			l=''
		else:
			if l[-1]=='\\': l=l[:-1]
			nest=beginnest
	#pprint.pprint(qm)

	# process statements sequentially, replace variables / functions
	for stat in qm:
		# discard what should be discarded
		discard=False
		for pat in discardedStatements:
			if match(pat,stat):
				discard=True; break
		if discard: continue
		#replace environment and qmake variables
		while True:
			m=search(r'\$\$?([a-zA-Z0-9_]+\b|\([a-zA-Z0-9_]+\)|\{[a-zA-Z0-9_]+\})',stat) # possible patters of variables
			if not m: break

			v=m.group(1)
			if v[0]=='{':
				v=v[1:-1]
				if os.environ.has_key(v):
					repl=os.environ[v]
					info("Substituting environment variable `%s` → `%s`"%(v,repl))
				else:
					if not v in undefOK: warning2("Undefined environment variable `%s'."%(v))
					repl=''
			else:
				if v[0]=='(': v=v[1:-1]
				if vars.has_key(v): repl=vars[v]
				else:
					if not v in undefOK:
						if os.environ.has_key(v):
							repl=os.environ[v]
							if not v in replaceOK: warning2("Undefined internal variable `%s', using environment variable instead."%(v))
						else:
							warning2("Undefined internal variable `%s'."%(v))
							repl=''
					else: repl=''
			stat=stat[:m.start()]+repl+stat[m.end():]

		# scons variables (namely, $PREFIX, were smuggled unexpanded by using @PREFIX instead. Change it to what it should be now.
		stat=stat.replace('@','$')

		conditional=match(r'^\s*(!?[a-zA-Z0-9_]+)\s*{(.*)}$',stat) # this is a (possibly negated) conditional; nothing complicated, please; specifically, nested conditionals do not work
		if conditional:
			cond,then=conditional.group(1,2)
			if cond=='win32': continue # windows stuff is discarded completely
			elif cond=='!win32': stat=then # otherwise we pass the whole then-clause to further treatment
			else: warning2("Unknown condition `%s'."%cond)
		assign=match(r'^\s*([a-zA-Z0-9_.]+)\s*(=|\+=)\s*(.*)$',stat) # this line is an assignment
		call=match(r'^\s*(!?[a-z][a-zA-Z0-9_]+)\s*(\(.*)$',stat) # this line is a "function call"
		if assign:
			param,action,val=assign.group(1,2,3)
			val=sub('\s+$','',val)
			val=splitgroup(sub('\s+',' ',val))
			if not vars.has_key(param): vars[param]=[]
			if action =='=' or action=='+=':	vars[param]+=val
		elif call:
			func,rest=call.group(1,2)
			if not func in undefOK: warning2("Function `%s' not implemented; arguments `%s'.\n"%(func,rest))
		else:
			warning2("Line not parsed: `%s'.\n"%(stat))
	return vars

def proProcess(filename,_vars={},dir='.',nest=0):
	info("(%s) Processing project file `%s'."%(nest,filename))
	global currentPro
	currentPro=filename
	posterityVars=[]
	vars=deepcopy(_vars)
	for l in localVars:
		if vars.has_key(l): del vars[l] # these are NOT propagated to sub-builds
	childVars=parseProject(open(filename,'r'),vars)
	childVars['PROJECT']=filename
	assert(childVars.has_key('TEMPLATE')) # otherwise the file is broken
	assert(len(childVars['TEMPLATE'])==1) # dtto
	template=childVars['TEMPLATE'][0]
	if template=='subdirs': #
		if not childVars.has_key('SUBDIRS'):
			#warning("Template is `subdirs' but SUBDIRS is empty.")
			return None
		for subdir in childVars['SUBDIRS']:
			#print "dir=%s,subdir=%s"%(dir,subdir)
			subPro=dir+'/'+subdir+'/'+subdir.split('/')[-1]+'.pro' # .pro file is named the same as its immediate parent directory + .pro
			posterityVars.append(proProcess(subPro,_vars=childVars,dir=dir+'/'+subdir,nest=nest+1)) # recurse
		return [posterityVars]
	elif template=='app' or 'lib':
		guessedTarget=os.path.split(filename)[-1].split('.')[0] # garbage/foo.pro -> foo
		explicitTarget=None
		if childVars.has_key('TARGET'): explicitTarget=os.path.split(childVars['TARGET'][0])[1].split('.')[0]
		if explicitTarget: childVars['TARGET']=explicitTarget
		else: childVars['TARGET']=guessedTarget
		#if oldTarget!=newTarget: warning("Old and new target differ: `%s' vs. `%s' (using new one)."%(oldTarget,newTarget))
		return [childVars]
	else:
		warning2("%s: Unknown TEMPLATE `%s'"%(currentPro,template))
		return None

def listUnique(List):
	if len(List)<=1: return List
	List.sort()
	last=List[-1]
	for i in range(len(List)-2, -1, -1):
		if last==List[i]: del List[i]
		else: last=List[i]
	return List

def processVars(V,dir):
	ret=""
	for v in V:
		if isinstance(v,list):
			ret+=processVars(v,dir)
		if isinstance(v,dict):
			ret+=scriptGen(v,dir)
	return ret

def pythonicTarget(t):
	#return target name such that it is acceptable as python identifier
	return sub('(^[0-9]|^|[^a-zA-Z0-9_])','_',t)


def scriptGen(v,dir):
	project=v['PROJECT']
	global currentPro
	currentPro=project
	target=v['TARGET']
	commentOut=False # waf doesn't like targetless sources; scons is fine with that
	template=v['TEMPLATE'][0]

	###################
	### PATHS

	projAbsPath=realpath(normpath(dirname(project)))
	dirAbsPath=realpath(normpath(dir))
	prefix=commonprefix([projAbsPath,dirAbsPath])
	relPath=projAbsPath[len(prefix)+1:]
	#warning("PATHS:\n%s\n%s →\n%s"%(projAbsPath,dirAbsPath,relPath))
	assert(not isabs(relPath))
	#return 'PROJECT=%s\nTARGET=%s\nSOURCES=%s\nINCLUDES=%s\nrelPath=%s\n'%(v['PROJECT'],v['TARGET'],string.join(v['SOURCES']),string.join(v['INCLUDEPATH']),relPath)

	###################
	### SOURCES

	if v.has_key('SOURCES'): sources=v['SOURCES']
	else: sources=[];
	if v.has_key('FORMS'): sources+=v['FORMS']
	#if v.has_key('IDLS'): sources+=v['IDLS']
	sources=listUnique(sources)

	###################
	### LIBRARIES
	
	def prependDirFile(d,f): return map(lambda x: join(d,x),f)
	def listLibs(ll):
		ret=[]
		for l in ll:
			if l[0:2]=='-l': ret.append(l[2:])
			elif l=='-rdynamic': pass
			else:
				warning2("Unknown LIBS item `%s'."%l)
		return ret
	libs=[]
	if v.has_key('LIBS'): libs=listLibs(v['LIBS'])

	###################
	### INCLUDE PATHS

	# waf/scons don't compile from the current directory, make sure it is there; duplicata removed later
	if not v.has_key('INCLUDEPATH'): includePath=['.']
	else:	includePath=v['INCLUDEPATH']+['.']
	includePath=listUnique(includePath)
	#print "relPath=%s,INCLUDEPATH=%s"%(relPath,includePath)
	def prependDirFileIfRelative(d,f):
		def prepIf(ff):
			if not isabs(ff) and ff[0]!='$': return join(d,ff)
			return ff
		return map(prepIf,f)
	includePath=prependDirFileIfRelative(relPath,includePath)
	# filter out non-existent paths; without warning, it could be a short lambda...
	def DelAndWarnNonexistentPath(x):
		if not exists(normpath(join(dirAbsPath,x))) and x[0]!='$':
			warning2("Include path `%s' is invalid, removed!"%(normpath(join(dirAbsPath,x))))
			return False
		return True
	includePath=filter(DelAndWarnNonexistentPath,includePath)
	includePath=[normpath(p) for p in includePath]
	# remove duplicates... http://stinkpot.afraid.org:8080/tricks/index.php/2006/05/find-all-the-unique-elements-in-a-python-list/
	includePath=dict([(i,1) for i in includePath]).keys()



	# begin building the actual script
	ret="## %s\n"%v['PROJECT']

	if template=='lib': 
		if v.has_key('CONFIG') and 'dll' in v['CONFIG']: targetType='shlib'
		else: targetType='staticlib'
	elif template=='app': targetType='program'

	if buildEngine=='waf':
		assert(targetType) # "TEMPLATE is neither `lib' nor `app'.

		if not len(sources)==0:
			warning2("Project `%s' has empty source list, build rule will be commented out."%project)
			commentOut=True

		langType='cpp'
		localLibs,systemLibs=[],[]
		if target in targetLangType: langType=targetLangType[target]
		if langType=='qt3': systemLibs.append('QT3')

		ret+="obj=bld.create_obj('%s','%s')\n"%(langType,targetType)

		ret+="obj.name='%s'\n"%target
		ret+="obj.target='%s'\n"%target
		
		ret+="obj.source='%s'\n"%string.join(prependDirFile(relPath,sources))

		# FIXME: install not yet there
		#ret+="obj.install_in='%s'\n"%
		
		#includePath=filter(lambda x: exists(normpath(join(dirAbsPath,x))),includePath)

		ret+="obj.includes='%s'\n"%string.join(includePath)
		# libs
		if len(libs)>0:
			for l in libs:
				if l in allSystemLibs: systemLibs.append(l)
				else: localLibs.append(ll)
		if len(localLibs)>0:	ret+="obj.uselib_local='%s'\n"%(string.join(localLibs))
		if len(systemLibs)>0: ret+="obj.uselib='%s'\n"%(string.join(systemLibs))
	elif buildEngine=='scons':
		env=defaultEnv
		if target in targetEnv: env=targetEnv[target]
		ret+="%s.%s('%s',%s%s"%(env,{'program':'Program','shlib':'SharedLibrary','staticlib':'StaticLibrary'}[targetType],
			target,fieldSep,toStr(prependDirFile(relPath,sources)))
		if installable.has_key((env,targetType)): installable[(env,targetType)].append(pythonicTarget(target))
		else: installable[(env,targetType)]=[pythonicTarget(target),]
		if len(libs)>0:
			ret+=",%sLIBS=%s['LIBS']+%s"%(fieldSep,env,toStr(libs))
		if len(includePath)>0:
			# using CPPPATH would override top-level settings (question posted on scons ML how to avoid this)
			# for now, put all paths prefixed with -I to CPPFLAGS as workaround
			#ret+=",%sCPPFLAGS=%s"%(fieldSep,toStr(map(lambda x: '-I'+x,includePath)))
			### NO, that doesn't work since CPPPATH is relative to SConscript dir, whereas CPPFLAGS are not prepended (logically)
			### try the inverse:
			ret+=",%sCPPPATH=%s['CPPPATH']+%s"%(fieldSep,env,toStr(includePath))
		ret+=')'
		instDir=installDirs[targetType]
		if instDirTargets.has_key((env,instDir)): instDirTargets[(env,instDir)].append((ret,target))
		else: instDirTargets[(env,instDir)]=[(ret,target)]
		
	else: raise ValueError('buildEngine must be waf or scons (--waf or --scons)');


	if commentOut:
		ret='# This rule is commented out because it has no source'+sub('(^|\n)','\n#',ret)

	ret+='\n'
	return ret


def masterScriptGen(V,dir):
	#
	s=''
	if buildEngine=='scons':
		s+="Import('*')\n"
		processVars(V,dir) # return value of processVars discarded, results passed in instDirTarget instead
		for env,dir in instDirTargets.keys():
			idt=instDirTargets[(env,dir)]
			#binary targets are handles specially: they install under _filename_ postfixed
			if match('.*/bin$',dir): ## warhing: this is OS-specific to tell binary target!
				s+="%s.InstallAs(["%env+','.join(["'%s$POSTFIX'"%(join(dir,tgSpec[1])) for tgSpec in idt])+"],[\n"
				s+=',\n'.join([sub(r'(?m)^','\t',tgSpec[0]) for tgSpec in idt])
				s+='\n])'
			#whereas other targets install under _filename_ into a dir that has been postfixed already
			else:
				s+="%s.Install('%s',[\n"%(env,dir)
				s+=',\n'.join([sub(r'(?m)^','\t',tgSpec[0]) for tgSpec in idt])+'\n])\n'
				#for tgSpec in idt: print tgSpec[0]
	elif buildEngine=='waf':
		s+=processVars(V,dir)
	return s


assert(len(sys.argv)>=3)
buildEngine,projects,scriptDir=sys.argv[1][2:],sys.argv[2:-1],sys.argv[-1]
assert(buildEngine in ['waf','scons'])
allVars=[]
for project in projects:
	allVars.append(proProcess(project,dir=dirname(project)))

# HACK: useful stuff to pass down via globals...
pretty=True
if pretty:
	def toStr(what):
		ret=pprint.pformat(what); n=1
		# replace leading spaces (nesting) by tabs...
		while n: ret,n=subn(r'(?m)^(\t*) ',r'\1\t',ret)
		# indent all lines by one more tab
		ret=sub(r'(?m)^','\t',ret)
		# de-indent the first one
		return ret[1:]
	fieldSep='\n\t'
else:
	toStr=str; fieldSep='';
installable={} # hash indexed by (env,targetType)->pythonicTargetName
instDirTargets={} # hash indexed by (env,targetDir)->str_target_spec
installDirs={'shlib':join('$PREFIX','lib','yade$POSTFIX',string.split(scriptDir,os.sep)[-1]),'staticlib':join('$PREFIX','lib','yade$POSTFIX'),'program':join('$PREFIX','bin')}

print masterScriptGen(allVars,scriptDir)