Lumiera 0.pre.04
»edit your freedom«
Loading...
Searching...
No Matches
LumieraEnvironment.py
Go to the documentation of this file.
1# coding: utf-8
2
5
6# Copyright (C)
7# 2008-2025 Hermann Vosseler <Ichthyostega@web.de>
8#
9# **Lumiera** is free software; you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the
11# Free Software Foundation; either version 2 of the License, or (at your
12# option) any later version. See the file COPYING for further details.
13
14
15
16from os import path
17
18import SCons.SConf
19from SCons.Action import Action, FunctionAction
20from SCons.Script import File as SConsFile
21from SCons.Environment import Environment
22
23from Buildhelper import *
24
25
26
27class LumieraEnvironment(Environment):
28 """ Custom SCons build environment for Lumiera
29 This allows us to carry structured config data without
30 using global vars. Idea inspired by Ardour.
31 """
32 def __init__(self, buildSetup, buildVars, **kw):
33 Environment.__init__ (self, toolpath = [buildSetup.TOOLDIR ]
34 , variables = buildVars # ◁───── reads settings from the commandline (see Options.py)
35 , **kw)
36 #
37 self['TARGDIR'] = buildSetup.TARGDIR
38 self['VERSION'] = buildSetup.VERSION
39 self['DESTDIR'] = '$INSTALLDIR/$PREFIX'
40 self['SHARE' ] = '$DESTDIR/share'
41 self._anchor_relative('INSTALLDIR')
42 self._anchor_relative('TARGDIR')
43 #
44 self.path = Record (extract_localPathDefs(buildSetup)) # ◁───── e.g. buildExe -> env.path.buildExe
45 self.libInfo = {}
46 self.Tool("BuilderDoxygen")
47 self.Tool("ToolDistCC")
48 self.Tool("ToolCCache")
52
53 def _anchor_relative(self, key):
54 """ ensure that a relative path spec becomes anchored at build-root
55 @note: a special convention within scons: '#' implies directory of SConstruct
56 """
57 spec = self[key].strip()
58 if not (spec.startswith('/') or spec.startswith('#')):
59 spec = '#'+spec
60 self[key] = spec
61
62
63
64 def Configure (self, *args, **kw):
65 kw['env'] = self
66 return LumieraConfigContext(*args, **kw)
67
68 def mergeConf (self,other):
69 """ extract the library/compiler flags from other Environment.
70 Optionally accepts a list or just sting(s) representing keys
71 in our own libInfo Dictionary
72 """
73 if isinstance(other, list):
74 for elm in other:
75 self.mergeConf(elm)
76 elif isinstance(other, str):
77 if other in self.libInfo:
78 self.mergeConf(self.libInfo[other])
79 else:
80 self.Append (LIBS = other.get('LIBS',[]))
81 self.Append (LIBPATH = other.get('LIBPATH', []))
82 self.Append (CPPPATH = other.get('CPPPATH', []))
83 self.Append (LINKFLAGS = other.get('LINKFLAGS', []))
84
85 return self
86
87
88 def addLibInfo (self, libID, minVersion=0, alias=None):
89 """ use pkg-config to create an Environment describing the lib.
90 Don't add this defs to the current Environment, rather store
91 them in the libInfo Dictionary.
92 """
93 minVersion = str(minVersion)
94 if 0 != os.system('pkg-config --print-errors --exists "%s >= %s"' % (libID,minVersion)):
95 print("Problems configuring the Library %s (>= %s)" % (libID,minVersion))
96 return False
97
98 self.libInfo[libID] = libInfo = Environment()
99 libInfo["ENV"]["PKG_CONFIG_PATH"] = os.environ.get("PKG_CONFIG_PATH")
100 libInfo.ParseConfig ('pkg-config --cflags --libs '+ libID )
101 if alias:
102 self.libInfo[alias] = libInfo
103 return libInfo
104
105
106
107
108
109# extending the 'Configure' functionality of SCons,
110# especially for library dependency checking
111ConfigBase = SCons.SConf.SConfBase
112
113
114
116 """ Extends the SCons Configure context with some convenience methods
117 """
118 def __init__(self, *args,**kw):
119 ConfigBase.__init__(self,*args,**kw)
120
121 def CheckPkgConfig (self, libID, minVersion=0, alias=None):
122 print("Checking for library configuration: %s " % libID)
123 # self.Message(self,"Checking for library configuration: %s " % libID)
124 return self.env.addLibInfo (libID, minVersion, alias)
125
126
127
128
129
130
132
133
135 """ Registers a custom Builder for generating and installing Icons from SVG.
136 Additionally you need to build the tool (rsvg-convert.c)
137 used to generate png from the svg source using librsvg.
138 """
139
140 import IconSvgRenderer as renderer # load Joel's python script for invoking the rsvg-convert (SVG render)
141 renderer.rsvgPath = env.subst("$TARGDIR/rsvg-convert").removeprefix('#')
142 # # the prefix '#' is a SCons specific convention,
143 # # which the external tool can not handle
144 #
145 # MD5 signature for this specific python source code...
146 thisCodeSignature = SConsFile(__file__).get_csig() + SConsFile(renderer.__file__).get_csig()
147 thisCodeSignature = bytearray(thisCodeSignature, 'utf-8')
148
149
150 class IconRenderAction(FunctionAction):
151 """ SCons Action subclass to provide a controlled cache signature.
152 @note: usually it would be sufficient to pass just a callable to the Builder,
153 however, our implementation calls into an external Python module and thus
154 the default signature from SCons would not be stable, since it relies
155 on a code representation including memory addresses. Without this,
156 the icons would be frequently rebuilt unnecessarily.
157 """
158
159 def __init__(self):
160 FunctionAction.__init__(self, IconRenderAction.invokeRenderer
161 , {'cmdstr' : "rendering Icon: $SOURCE --> $TARGETS"}
162 )
163
164 def get_contents(self, target, source, env):
165 """ a stable signature based on the source code """
166 return thisCodeSignature
167
168 @staticmethod
169 def invokeRenderer(target, source, env):
170 """ render the SVG icon with libRSVG """
171 source = str(source[0])
172 targetdir = env.subst(env.path.buildIcon).removeprefix('#')
173 renderer.main([source,targetdir])
174 return 0
175
176
177 def createIconTargets(target,source,env):
178 """ parse the SVG to get the target file names """
179 source = str(source[0])
180 targetdir = env.path.buildIcon
181 targetfiles = renderer.getTargetNames(source) # parse SVG
182
183 # additionally create an installation task for each Icon to be generated
184 installLocation = env.path.installIcon
185 generateTargets = []
186 for icon in targetfiles:
187 icon = targetdir+icon
188 subdir = getDirname(str(icon))
189 env.Install (installLocation+subdir, icon)
190 generateTargets.append(icon)
191
192 return (generateTargets, source)
193
194
195 buildIcon = env.Builder( action = IconRenderAction()
196 , single_source = True
197 , emitter = createIconTargets
198 )
199 env.Append(BUILDERS = {'IconRender' : buildIcon})
200
201
202
203
205 """ Registers further Custom Methods for installing various Resources.
206 """
207
208 def IconResource(env, source):
209 """ copy icon pixmap to corresponding icon dir. """
210 subdir = getDirname(str(source))
211 toBuild = env.path.buildIcon+subdir
212 toInstall = env.path.installIcon+subdir
213 env.Install (toInstall, source)
214 return env.Install(toBuild, source)
215
216 def GuiResource(env, source):
217 """ pick up given source resource and install
218 them (flat) into the configured target
219 """
220 toBuild = env.path.buildUIRes
221 toInstall = env.path.installUIRes
222 env.Install (toInstall, source)
223 return env.Install(toBuild, source)
224
225 def ConfigData(env, prefix, source, targetDir=None):
226 """ install (copy) configuration- and metadata.
227 @param targetDir: when None, then use he install location configured (in Setup.py),
228 otherwise an explicitly given absolute or relative path segment,
229 which might refer to the location of the executable through the $ORIGIN token
230 @param prefix: a prefix relative to the current path (location of SConscript),
231 i.e. typically a subdirectory where to find the source config file
232 """
233 source = path.join(prefix,str(source))
234 subdir = getDirname(source, prefix) # removes source location path prefix
235 if targetDir:
236 if path.isabs(targetDir):
237 toBuild = toInstall = path.join(targetDir,subdir)
238 else:
239 if targetDir.startswith('$ORIGIN'):
240 targetDir = targetDir[len('$ORIGIN'):]
241 toBuild = path.join(env.path.buildExe, targetDir, subdir)
242 toInstall = path.join(env.path.installExe, targetDir, subdir)
243 else:
244 toBuild = path.join(env.path.buildConf, targetDir, subdir)
245 toInstall = path.join(env.path.installConf, targetDir, subdir)
246 else:
247 toBuild = path.join(env.path.buildConf,subdir)
248 toInstall = path.join(env.path.installConf,subdir)
249 env.Install (toInstall, source)
250 return env.Install(toBuild, source)
251
252 def DocFile(env, prefix, source, target=None):
253 """ install (copy) files for documentation.
254 Always places the documentation below the standard location 'installDoc' configured in Setup.py
255 @param prefix: relative to current path (SConscript), will be stripped at destination
256 @param target: when given, the target will be named explicitly, or (when only a directory)
257 placed into a specific subdir, otherwise (when None) the source spec will be placed
258 into the corresponding subdir after stripping the prefix
259 """
260 source = path.join(prefix,str(source))
261 subdir = getDirname(source, prefix) # removes source location path prefix
262 if not target:
263 target = subdir+'/'
264 elif target.endswith('/'):
265 target = target+subdir+'/'
266 toInstall = path.join(env.path.installDoc, target)
267 if toInstall.endswith('/'):
268 return env.Install(toInstall, source)
269 else:
270 return env.InstallAs(toInstall, source) # this renames at target
271
272
273 env.AddMethod(IconResource)
274 env.AddMethod(GuiResource)
275 env.AddMethod(ConfigData)
276 env.AddMethod(DocFile)
277
278
279
280
281class WrappedStandardExeBuilder(SCons.Util.Proxy):
282 """ Helper to add customisations and default configurations to SCons standard builders.
283 The original builder object is wrapped and most calls are simply forwarded to this
284 wrapped object by Python magic. But some calls are intercepted in order to inject
285 suitable default configuration based on the project setup.
286 """
287
288 def __init__(self, originalBuilder):
289 SCons.Util.Proxy.__init__ (self, originalBuilder)
290
291 def __bool__(self): return True
292
293 def __call__(self, env, target=None, source=None, **kw):
294 """ when the builder gets invoked from the SConscript...
295 create a clone environment for specific configuration
296 and then pass on the call to the wrapped original builder.
297 Automatically define installation targets for build results.
298 @note only returning the build targets, not the install targets
299 """
300 customisedEnv = self.getCustomEnvironment(env, target=target, **kw) # defined in subclasses
301 buildTarget = self.buildLocation(customisedEnv, target)
302 buildTarget = self.invokeOriginalBuilder(customisedEnv, buildTarget, source, **kw)
303 self.installTarget(customisedEnv, buildTarget, **kw)
304 return buildTarget
305
306
307 def invokeOriginalBuilder(self, env, target, source, **kw):
308 return self.get().__call__ (env, target, source, **kw)
309
310 def buildLocation(self, env, target):
311 """ prefix project output directory """
312 prefix = self.getBuildDestination(env)
313 return list(prefix+str(name) for name in target)
314
315 def installTarget(self, env, buildTarget, **kw):
316 """ create an additional installation target
317 for the generated executable artifact
318 """
319 indeedInstall = lambda p: p and p.get('install')
320
321 if indeedInstall(kw):
322 return env.Install (dir = self.getInstallDestination(env), source=buildTarget)
323 else:
324 return []
325
326
327
328
330
331 def getCustomEnvironment(self, lumiEnv, **kw):
332 """ augments the built-in Program() builder to add a fixed rpath based on $ORIGIN
333 That is: after searching LD_LIBRARY_PATH, but before the standard linker search,
334 the directory relative to the position of the executable ($ORIGIN) is searched.
335 This search path is active not only for the executable, but for all libraries
336 it is linked with.
337 @note: enabling the new ELF dynamic tags. This causes a DT_RUNPATH to be set,
338 which results in LD_LIBRARY_PATH being searched *before* the RPATH
339 """
340 custEnv = lumiEnv.Clone()
341 custEnv.Append( LINKFLAGS = "-Wl,-rpath=\\$$ORIGIN/modules,--enable-new-dtags" )
342 if 'addLibs' in kw:
343 custEnv.Append(LIBS = kw['addLibs'])
344 return custEnv
345
346 def getBuildDestination(self, lumiEnv): return lumiEnv.path.buildExe
347 def getInstallDestination(self, lumiEnv): return lumiEnv.path.installExe
348
349
350
351
353
354 def getCustomEnvironment(self, lumiEnv, target, **kw):
355 """ augments the built-in SharedLibrary() builder to add some tweaks missing in SCons 1.0,
356 like setting a SONAME proper instead of just passing the relative pathname to the linker.
357 Besides, we override the library search path to allow for transitive dependencies between
358 Lumiera modules; modules are assumed to reside in a subdirectory below the executable.
359 """
360 custEnv = lumiEnv.Clone()
361 custEnv.Append( LINKFLAGS = "-Wl,-soname="+self.defineSoname(target,**kw))
362 custEnv.Append( LINKFLAGS = "-Wl,-rpath=\\$$ORIGIN/../modules,--enable-new-dtags" )
363 if 'addLibs' in kw:
364 custEnv.Append(LIBS = kw['addLibs'])
365 return custEnv
366
367 def getBuildDestination(self, lumiEnv): return lumiEnv.path.buildLib
368 def getInstallDestination(self, lumiEnv): return lumiEnv.path.installLib
369
370
371 def installTarget(self, env, buildTarget, **kw):
372 """ ensure a shared library is not marked executable.
373 The default toolchain on Linux often installs shared libraries as executable, which seems
374 to be necessary on some arcane Unix platforms. However, Debian Policy prohibits that.
375 See https://unix.stackexchange.com/questions/400187/why-should-or-should-not-shared-libraries-be-executable-e-g-red-hat-vs-debian
376 """
377 toInstall = super().installTarget(env, buildTarget, **kw)
378 if toInstall:
379 def _Chmod(target, source, env):
380 """ Workaround since env.Chmod is present only in SCons 4.10 """
381 import os
382 for t in target:
383 os.chmod(str(t), 0o644)
384 return None
385# removeExecBit = env.Chmod(toInstall, 0o644) # ◁◁◁ could use this for SCons > 4.10
386 msg = '....... clear exec perm %s' % [str(t) for t in toInstall]
387 removeExecBit = env.Action(_Chmod, msg)
388 env.AddPostAction(toInstall, removeExecBit)
389 return toInstall
390
391 def defineSoname (self, target, **kw):
392 """ internal helper to extract or guess
393 a suitable library SONAME, either using an
394 explicit spec, falling back on the lib filename
395 """
396 if 'soname' in kw:
397 soname = self.subst(kw['soname']) # explicitly defined by user
398 else: # else: use the library filename as DT_SONAME
399 if SCons.Util.is_String(target):
400 pathname = target.strip()
401 elif 1 == len(target):
402 pathname = str(target[0]).strip()
403 else:
404 raise SyntaxError("Lumiera Library builder requires exactly one target spec. Found target="+str(target))
405
406 assert pathname
407 (dirprefix, libname) = path.split(pathname)
408 if not libname:
409 raise ValueError("Library name missing. Only got a directory: "+pathname)
410
411 soname = "${SHLIBPREFIX}%s$SHLIBSUFFIX" % libname
412
413 assert soname
414 return soname
415
416
417
419
420 def getCustomEnvironment(self, lumiEnv, target, **kw):
421 """ in addition to the ModuleBuilder, define the Lumiera plugin suffix
422 """
423 custEnv = LumieraModuleBuilder.getCustomEnvironment(self, lumiEnv, target, **kw)
424 custEnv.Append (CPPDEFINES='LUMIERA_PLUGIN')
425 custEnv.Replace(SHLIBPREFIX='', SHLIBSUFFIX='.lum')
426 return custEnv
427
428 def getBuildDestination(self, lumiEnv): return lumiEnv.path.buildPlug
429 def getInstallDestination(self, lumiEnv): return lumiEnv.path.installPlug
430
431
432
433
434
435
437 """ install the customised builder versions tightly integrated with our build system.
438 Especially, these builders automatically add the build and installation locations
439 and set the RPATH and SONAME in a way to allow a relocatable Lumiera directory structure
440 """
441 programBuilder = LumieraExeBuilder (lumiEnv['BUILDERS']['Program'])
442 libraryBuilder = LumieraModuleBuilder (lumiEnv['BUILDERS']['SharedLibrary'])
443 smoduleBuilder = LumieraModuleBuilder (lumiEnv['BUILDERS']['LoadableModule'])
444 lpluginBuilder = LumieraPluginBuilder (lumiEnv['BUILDERS']['LoadableModule'])
445
446 lumiEnv['BUILDERS']['Program'] = programBuilder
447 lumiEnv['BUILDERS']['SharedLibrary'] = libraryBuilder
448 lumiEnv['BUILDERS']['LoadableModule'] = smoduleBuilder
449 lumiEnv['BUILDERS']['LumieraPlugin'] = lpluginBuilder
450
451
452 def SymLink(env, target, source, linktext=None):
453 """ use python to create a symlink
454 """
455 def makeLink(target,source,env):
456 if linktext:
457 dest = linktext
458 else:
459 dest = str(source[0])
460 link = str(target[0])
461 os.symlink(dest, link)
462
463 if linktext: srcSpec=linktext
464 else: srcSpec='$SOURCE'
465 action = Action(makeLink, "Install link: $TARGET -> "+srcSpec)
466 env.Command (target,source, action)
467
468 # adding SymLink directly as method on the environment object
469 # Probably that should better be a real builder, but I couldn't figure out
470 # how to get the linktext through literally, which is necessary for relative links.
471 # Judging from the sourcecode of SCons.Builder.BuilderBase, there seems to be no way
472 # to set the executor_kw, which are passed through to the action object.
473 lumiEnv.AddMethod(SymLink)
Extends the SCons Configure context with some convenience methods.
CheckPkgConfig(self, libID, minVersion=0, alias=None)
Custom SCons build environment for Lumiera This allows us to carry structured config data without usi...
mergeConf(self, other)
extract the library/compiler flags from other Environment.
__init__(self, buildSetup, buildVars, **kw)
_anchor_relative(self, key)
ensure that a relative path spec becomes anchored at build-root
addLibInfo(self, libID, minVersion=0, alias=None)
use pkg-config to create an Environment describing the lib.
getCustomEnvironment(self, lumiEnv, **kw)
augments the built-in Program() builder to add a fixed rpath based on $ORIGIN That is: after searchin...
defineSoname(self, target, **kw)
internal helper to extract or guess a suitable library SONAME, either using an explicit spec,...
getCustomEnvironment(self, lumiEnv, target, **kw)
augments the built-in SharedLibrary() builder to add some tweaks missing in SCons 1....
installTarget(self, env, buildTarget, **kw)
ensure a shared library is not marked executable.
getCustomEnvironment(self, lumiEnv, target, **kw)
in addition to the ModuleBuilder, define the Lumiera plugin suffix
Helper to add customisations and default configurations to SCons standard builders.
invokeOriginalBuilder(self, env, target, source, **kw)
installTarget(self, env, buildTarget, **kw)
create an additional installation target for the generated executable artifact
buildLocation(self, env, target)
prefix project output directory
extract_localPathDefs(localDefs)
extracts the directory configuration values.
getDirname(d, basePrefix=None)
extract directory name without leading path, or without the explicitly given basePrefix
register_LumieraIconBuilder(env)
Registers a custom Builder for generating and installing Icons from SVG.
register_LumieraCustomBuilders(lumiEnv)
install the customised builder versions tightly integrated with our build system.
register_LumieraResourceBuilders(env)
Registers further Custom Methods for installing various Resources.