Please note:The SCons wiki is in read-only mode due to ongoing spam/DoS issues. Also, new account creation is currently disabled. We are looking into alternative wiki hosts.

SCons and Eiffel

This is a SCons tool for building EiffelStudio projects. It works with EiffelStudio 5.7 and above (although the latest EiffelStudio is recommended, currently 7.1). It has been tested on Windows (XP, 2003 and 7), Mac OS X (10.4, 10.5, 10.6 and 10.7) and Linux (Ubuntu 7.10 and 10.04).

Put the builder in a script called Eiffel.py, in a directory that you reference with toolpath, as shown in the example below; or put it in a directory called site_scons/site_tools as mentioned in the SCons man page.

It requires Python 2.4 or above, because it uses the subprocess module which was new in Python 2.4.

Sources and Targets

This Eiffel builder knows the nuts and bolts of running EiffelStudio's command-line compiler. Each EiffelStudio project has a .ecf file (Eiffel configuration file) and builds an executable or a DLL. You feed the builder the path to the .ecf configuration file; this is the builder's source. You can specify additional source dependencies, if you wish (resource files, icons, etc.).

The .ecf file contains one or more targets. A .ecf target specifies which executable you want to build and how to build it. You specify one of these to the builder.

The target passed to the builder is not the same as the target that the builder emits. The target passed in is a string (e.g., 'foo'), from which the builder computes the emitted full target path (e.g., 'C:\projects\app\EIFGENs\foo\F_code\foo.exe'). The builder's emitter computes the full target path by reading various settings inside the .ecf file. (For example, the executable_name setting gives the base name of the target file; sometimes this setting is missing, in which case the base name is the same as the .ecf document element's name attribute. The target file extension is also computed; for example, it is .dll if the msil_generation_type setting is dll.)

When calling the builder, you may omit the target if you wish. By default, the Eiffel builder builds the target with the same base name as the .ecf file. (There will be an error, of course, if the .ecf file has no such target.)

The builder usually emits one target file path (the executable or dll). In some cases, however, it emits more.

The Scanner

Apart from the .ecf file, the most important input to the Eiffel compiler is the universe of .e Eiffel class files that it has to compile. These class files are dependencies that the builder figures out for itself, by scanning the .ecf file. The Scanner looks for all cluster and override declarations inside the .ecf, and finds all .e files in each such directory. Therefore, thanks to the Scanner, if you modify an Eiffel class in one of the clusters named in the .ecf, or if you modify the .ecf itself, then the builder will know that it needs to rebuild the project.

If the cluster or override is recursive, then the Scanner scans the subdirectories too. If it's not recursive but has explicitly-named sub-clusters, the Scanner also understands how to scan these sub-clusters.

As well as .e class files, the scanner detects the following dependencies mentioned in the .ecf file:

Environment variable substitution is performed on the file and directory paths. The Scanner prints warnings if the .ecf file uses undefined environment variables. Construction variables are deliberately ignored, because it would be incorrect to substitute them given that the Eiffel compiler does not know about SCons construction variables. The following EiffelStudio environment variables are commonly used in .ecf files, so if they are undefined the builder tries to define them with sensible platform-specific assumptions:

Choosing an Eiffel Compiler

The builder has a construction variable EC, specifying the Eiffel command-line compiler. It defaults to the path to "ec" found in the PATH environment variable. Failing that, it uses the ISE_EIFFEL environment variable to construct the path to "ec". If this too fails, then it falls back simply to "ec". (Using ISE_EIFFEL is helpful, for example, when running SCons on Linux or Mac via sudo, which restricts the PATH variable, or if the user has not added "ec" to the path.)

EiffelStudio 6.2 introduced an alternative compiler called "ecb". Its output is identical to the output of "ec", but it reportedly runs about 20% faster. Beware, however, that "ecb" uses its own set of precompiled libraries, rather than the ones installed with EiffelStudio, so if you are using precompiled libraries you will have to build them specially for "ecb".

I haven't tested the builder with the Gobo Eiffel compiler gec, nor with Helmut Brandl's Eiffel interpreter and compiler tecomp.

Eiffel Compiler Flags

The ECFLAGS construction variable controls what kind of build is done by the Eiffel compiler. Possible values for ECFLAGS are given by typing ec -help at the command line. Here are some common examples:

Note, in the last example, that specifying the -target flag overrides any other way of specifying the target.

ECFLAGS can be set by several means:

Logging

The Eiffel compiler 'ec' produces a lot of progress output. Generally you don't want to see this, so the Eiffel builder redirects it to a file, "SCons.Eiffel.log", in the same directory as the SConstruct. You can specify a different file by setting the ECLOG construction variable. If you want to let it to go to standard output, set ECLOG to nothing (e.g., on the command line, scons ECLOG=).

But you do want to see this output if the build fails! You can open the log file to inspect the reason for failure, which would normally be at the end of the log file. For convenience, however, the Eiffel builder echoes the last thousand characters from the log file onto standard output, which almost invariably proves to be just the right amount of detail to allow you to see what went wrong.

Example Usage

The following SConstruct assumes that you have created "app.ecf" with 3 targets:

   1 # Create an Environment that imports "Eiffel.py" from the same directory as the SConstruct.
   2 
   3 import os
   4 env = Environment(ENV = os.environ, tools = ['Eiffel'], toolpath = ['.'])
   5 
   6 # Build "app" (or, on Windows, "app.exe").
   7 # The build depends on "app.ecf" and "app.rc".
   8 # It also depends on all of the "*.e" files, etc., found by the Scanner.
   9 # (Note how the target does not need to be specified, because by default it builds the target with the same base name "app".)
  10 
  11 env.Eiffel(['app.ecf', 'app.rc'])
  12 
  13 # Build the precompiled Gobo library.
  14 # (Note how we specify the "gobo" target explicitly.)
  15 
  16 gobo = env.Eiffel('gobo', 'app.ecf', ECFLAGS = '-precompile -clean -c_compile')
  17 
  18 # Here's how to build "app" with the Gobo precompiled library.
  19 # (Note how it depends on "gobo", and the target is explicitly given.)
  20 
  21 env.Eiffel('app_using_precompile', ['app.ecf', 'app.rc', gobo])

Builder

   1 # EiffelStudio support for SCons
   2 # Written by Peter Gummer, February 2007
   3 # Scanner added - Peter Gummer, May 2007
   4 # Scanner uses xml.dom - Peter Gummer, September 2007
   5 # Fix scanning non-recursive clusters - Peter Gummer, March 2008
   6 # Don't call finish_freezing - Peter Gummer, April 2008
   7 # Support -target and -project_path compiler options and incremental builds - Peter Gummer, May 2008
   8 # Scanner detects more dependencies - Peter Gummer, June 2008
   9 # Emitter computes target by reading .ecf file - Peter Gummer, June 2008
  10 # Support -c_compile compiler option rather than hard-coding it - Peter Gummer, December 2008
  11 # Fix scanning on non-Windows platforms - Peter Gummer, August 2010
  12 # Emitter computes target compatibly with EiffelStudio 6.5 and higher - Peter Gummer, August 2010
  13 # Scanner detects "tests" dependencies to be compatible with EiffelStudio 6.7 - Peter Gummer, December 2010
  14 # The EC variable defaults from the the PATH or ISE_EIFFEL environment variable - Peter Gummer, December 2010
  15 # Scanner expands $ECF_CONFIG_PATH in dependency locations - Peter Gummer, May 2011
  16 # Adjust manifest file path in Windows resource file - Peter Gummer, July 2012
  17 # Remove the Files() method; use Glob() instead - Peter Gummer, January 2013
  18 
  19 """
  20 Tool-specific initialisation for EiffelStudio.
  21 This does not work with EiffelStudio 5.6 or earlier.
  22 """
  23 
  24 import os, glob, sys, shutil, datetime, subprocess, re, xml.dom.minidom
  25 from SCons.Script import *
  26         
  27 log_file = None
  28 
  29 def log_open(env):
  30         global log_file
  31 
  32         if env['ECLOG'] == '':
  33                 log_file = sys.stdout
  34         elif log_file == None:
  35                 log_file = open(env['ECLOG'], 'w+')
  36         elif log_file.closed:
  37                 log_file = open(env['ECLOG'], 'a+')
  38 
  39 def log(s):
  40         log_file.write(str(s) + '\n')
  41 
  42 def log_date():
  43         log(datetime.datetime.now())
  44 
  45 def log_process(args):
  46         commandline = subprocess.list2cmdline(args)
  47         if log_file != sys.stdout: print '  ' + commandline
  48         log(commandline)
  49         log_file.flush()
  50         subprocess.call(args, stdout = log_file, stderr = subprocess.STDOUT)
  51 
  52 def log_file_tail():
  53         """The last thousand characters of the log file."""
  54         result = ''
  55 
  56         if log_file != sys.stdout:
  57                 if log_file.tell() > 1000:
  58                         log_file.seek(-1000, 1)
  59                 else:
  60                         log_file.seek(0)
  61 
  62                 result = '... ' + log_file.read()
  63                 log_file.seek(0, 2)
  64 
  65         return result
  66 
  67 def ec_action(target, source, env):
  68         """
  69         The Eiffel Builder's action function, running the Eiffel compiler.
  70         Parameters are as returned by ec_emitter():
  71          * target: the paths to the files to be built, as generated by ec_emitter().
  72          * source[0]: the ECF file.
  73          * source[1], source[2], etc.: any additional dependencies.
  74          * env['ECLOG']: name of file to which all compiler output is logged (stdout if empty).
  75          * env['ECFLAGS']: Eiffel compiler flags: -finalize, -freeze, -clean, -project_path, -target, etc.
  76         Result is 0 (success) if all targets are built; else 1.
  77         (Note that the Eiffel compiler's return code is unreliable: it returns 0 if C compilation fails.)
  78         """
  79         result = 0
  80 
  81         log_open(env)
  82         log('=================== ' + ecf_target(target) + ' ===================')
  83         log_date()
  84 
  85         rc_copied_to_target = None
  86 
  87         if env['PLATFORM'] == 'win32':
  88                 rc = os.path.splitext(str(source[0]))[0] + '.rc'
  89 
  90                 if os.path.exists(rc):
  91                         project_path = dirname(str(target[0]), 4)
  92                         rc_copied_to_target = os.path.join(project_path, os.path.basename(rc))
  93 
  94                         if rc == rc_copied_to_target:
  95                                 rc_copied_to_target = None
  96                         else:
  97                                 f = open(rc, 'r')
  98                                 try: s = f.read()
  99                                 finally: f.close()
 100 
 101                                 if s:
 102                                         icon_pattern = r'(\w+[ \t]+ICON[ \t]+[^"]*")([^"]+")'
 103                                         manifest_pattern = r'(CREATEPROCESS_MANIFEST_RESOURCE_ID[ \t]+RT_MANIFEST[ \t]+")([^"]+")'
 104                                         substitution = r'\g<1>' + os.path.dirname(rc).replace('\\', '/') + r'/\g<2>'
 105                                         s = re.sub(icon_pattern, substitution, s)
 106                                         s = re.sub(manifest_pattern, substitution, s)
 107                                         f = open(rc_copied_to_target, 'w')
 108                                         try: f.write(s)
 109                                         finally: f.close()
 110 
 111         flags = env['ECFLAGS'].split()
 112         if not '-target' in flags: flags += ['-target', ecf_target(target)]
 113         log_process([env['EC'], '-batch', '-config', str(source[0])] + flags)
 114 
 115         for t in target:
 116                 if result == 0 and not os.path.exists(str(t)):
 117                         print log_file_tail()
 118                         result = 1
 119 
 120         if rc_copied_to_target: os.remove(rc_copied_to_target)
 121         if log_file != sys.stdout: log_file.close()
 122         return result
 123 
 124 def ec_emitter(target, source, env):
 125         """
 126         The Eiffel Builder's emitter function.
 127         Parameters:
 128          * target[0]: the ECF target to be built; if empty then defaults to the first target in the ECF file.
 129          * source[0]: the ECF file. The paths to the files to be built are computed by reading this file.
 130          * source[1], source[2], etc.: additional optional dependencies (precompiled libraries, ".rc" files, etc.).
 131          * env['ECFLAGS']: Eiffel compiler flags. The paths to the files to be built are affected by these.
 132         Result emits the target and source parameters passed to ec_action().
 133          * The source emitted is exactly the same as the source parameter passed in.
 134          * The target emitted contains one or more calculated file paths.
 135            Each target file is in the directory {-project_path}/EIFGENs/{-target}/{-finalize}, where:
 136                 -project_path if omitted defaults to the current working directory;
 137                 -target if omitted defaults to the base name of target[0] (or else to the first target in the ECF file);
 138                 -finalize evaluates to "F_code", else if omitted defaults to "W_code".
 139            The number of target file paths emitted, and the actual file names, depend on several factors:
 140             * Options specified inside the ECF file;
 141             * The -precompile flag;
 142             * The -c_compile flag.
 143         """
 144         result = None
 145 
 146         if len(target) > 0:
 147                 ec_target = os.path.basename(str(target[0]))
 148         else:
 149                 ec_target = ""
 150 
 151         if len(source) == 0:
 152                 print '****** ERROR! No source .ecf file specified: cannot build ' + ec_target
 153         elif not env.Detect(env['EC']):
 154                 print '****** ERROR! The Eiffel compiler ' + env['EC'] + ' is missing from your path: cannot build ' + ec_target
 155         else:
 156                 ecf = str(source[0])
 157                 ec_path = os.getcwd()
 158                 ec_code = 'W_code'
 159                 exe_name = dotnet_type = is_dotnet = is_precompiling = is_c_compiling = is_shared_library = None
 160 
 161                 flags = env['ECFLAGS'].split()
 162 
 163                 for i, flag in enumerate(flags):
 164                         if flag == '-project_path':
 165                                 ec_path = flags[i + 1]
 166                         elif flag == '-target':
 167                                 ec_target = flags[i + 1]
 168                         elif flag == '-finalize':
 169                                 ec_code = 'F_code'
 170                         elif flag == '-precompile':
 171                                 is_precompiling = True
 172                         elif flag == '-c_compile':
 173                                 is_c_compiling = True
 174 
 175                 ecf_as_xml = xml.dom.minidom.parse(ecf)
 176                 ec_target_next = ec_target
 177 
 178                 while ec_target_next <> None:
 179                         t = ec_target_next
 180                         ec_target_next = None
 181 
 182                         for element in ecf_as_xml.getElementsByTagName('target'):
 183                                 name = element.attributes['name'].value
 184                                 if ec_target == "": t = ec_target = name
 185 
 186                                 if t == name:
 187                                         if element.hasAttribute('extends'):
 188                                                 ec_target_next = element.attributes['extends'].value
 189 
 190                                         for setting in element.getElementsByTagName('setting'):
 191                                                 name = setting.attributes['name'].value
 192 
 193                                                 if name == 'msil_generation':
 194                                                         if is_dotnet == None:
 195                                                                 is_dotnet = setting.attributes['value'].value == 'true'
 196                                                 elif name == 'msil_generation_type':
 197                                                         if dotnet_type == None:
 198                                                                 dotnet_type = '.' + setting.attributes['value'].value
 199                                                 elif name == 'executable_name':
 200                                                         if exe_name == None:
 201                                                                 exe_name = setting.attributes['value'].value
 202                                                 elif name == 'shared_library_definition':
 203                                                         if is_shared_library == None:
 204                                                                 is_shared_library = True
 205 
 206                 if exe_name == None:
 207                         exe_name = str(ecf_as_xml.documentElement.attributes['name'].value)
 208 
 209                 if dotnet_type:
 210                         ext = dotnet_type
 211                 elif is_precompiling:
 212                         ext = '.melted'
 213                 elif is_shared_library:
 214                         exe_name = env['SHLIBPREFIX'] + exe_name
 215                         ext = env['SHLIBSUFFIX']
 216                 else:
 217                         exe_name = env['PROGPREFIX'] + exe_name
 218                         ext = env['PROGSUFFIX']
 219 
 220                 ec_path += '/EIFGENs/' + ec_target + '/'
 221 
 222                 if is_c_compiling:
 223                         ec_path += ec_code + '/'
 224                         result = [ec_path + exe_name + ext]
 225 
 226                         if is_dotnet:
 227                                 result += [ec_path + 'lib' + exe_name + '.dll']
 228                         elif is_precompiling:
 229                                 result += [ec_path + environment_variable(env, 'ISE_C_COMPILER') + '/' + env['PROGPREFIX'] + 'driver' + env['PROGSUFFIX']]
 230                         elif is_shared_library and env['PLATFORM'] == 'win32':
 231                                 result += [ec_path + 'dll_' + exe_name + '.lib']
 232                 else:
 233                         result = [ec_path + ec_code + '/Makefile.SH', ec_path + 'project.epr']
 234 
 235         return result, source
 236 
 237 ecf_environment_variable_regex = re.compile(r'(\$\||\$\(?\w*\)?|[^$]+)', re.M)
 238 
 239 def ecf_scanner(node, env, path):
 240         """
 241         All dependencies mentioned in 'node', which is expected to be an ECF file.
 242         The dependencies consist of:
 243          * All Eiffel class files found in all clusters (including override clusters) mentioned in the ECF file.
 244          * All .ecf library files mentioned in the ECF file. (Such libraries are not themselves scanned).
 245          * All .NET assemblies mentioned in the ECF file.
 246          * All external object files mentioned in the ECF file.
 247          * All .h and .hpp files found in external include directories mentioned in the ECF file.
 248         Because this ignores targets and conditionals in the ECF file, it may cause unnecessary builds.
 249         """
 250 
 251         def element_location(element):
 252                 """
 253                 The 'location' attribute of 'element', processed to take care of:
 254                  * Expansion of environment variables.
 255                  * If 'location' is relative, prefixing with the directory name of 'node'.
 256                  * If 'location' is a nested cluster, prefixing with the location of the parent element (recursively).
 257                 """
 258                 result = ''
 259                 ecf_config_path = os.path.dirname(os.path.abspath(str(node)))
 260 
 261                 for token in ecf_environment_variable_regex.findall(element.attributes['location'].value):
 262                         if token[0] <> r'$':
 263                                 result += token
 264                         elif token == r'$|':
 265                                 result += element_location(element.parentNode) + '/'
 266                         elif token == r'$ECF_CONFIG_PATH' or token == r'$(ECF_CONFIG_PATH)':
 267                                 result += ecf_config_path
 268                         else:
 269                                 s = environment_variable(env, token)
 270 
 271                                 if s:
 272                                         result += s
 273                                 else:
 274                                         print '****** WARNING!', str(node), 'uses undefined environment variable', token
 275 
 276                 result = result.replace('\\', '/')
 277 
 278                 if not os.path.isabs(result):
 279                         result = os.path.join(ecf_config_path, result)
 280 
 281                 return result
 282 
 283         result = []
 284         ecf_as_xml = xml.dom.minidom.parse(str(node))
 285 
 286         for tag in ['cluster', 'override', 'tests', 'library', 'assembly', 'external_include', 'external_object']:
 287                 for element in ecf_as_xml.getElementsByTagName(tag):
 288                         location = element_location(element)
 289 
 290                         if os.path.isfile(location):
 291                                 result += [location]
 292                         elif tag == 'external_include':
 293                                 result += env.Glob(location + '/*.h') + env.Glob(location + '/*.hpp')
 294                         elif element.attributes.get('recursive', None):
 295                                 result += classes_in_cluster(env, location)
 296                         else:
 297                                 result += env.Glob(location + '/*.e')
 298 
 299         return result
 300 
 301 def ecf_target(target, source = None, env = None):
 302         """The ECF target corresponding to the given build target."""
 303         return os.path.basename(dirname(str(target[0]), 2))
 304 
 305 def generate(env):
 306         """Add a Builder and construction variables for Eiffel to the given Environment."""
 307         default_ec_path = env.WhereIs('ec')
 308 
 309         if not default_ec_path:
 310                 default_ec_path = spec_path(env, 'studio', 'bin/ec')
 311                 if default_ec_path == '': default_ec_path = 'ec'
 312 
 313         vars = Variables()
 314         vars.Add('EC', "The Eiffel command-line compiler.", default_ec_path)
 315         vars.Add('ECFLAGS', "Use ec -help to see possible options.", '-finalize -clean -c_compile')
 316         vars.Add('ECLOG', "File to log Eiffel compiler output.", 'SCons.Eiffel.log')
 317         vars.Update(env)
 318         Help(vars.GenerateHelpText(env))
 319 
 320         env['BUILDERS']['Eiffel'] = Builder(action = Action(ec_action, ecf_target), emitter = ec_emitter, target_factory = Entry)
 321         env.Append(SCANNERS = Scanner(ecf_scanner, skeys = ['.ecf']))
 322         env.AddMethod(environment_variable, "EiffelEnvironmentVariable")
 323         env.AddMethod(classes_in_cluster, "EiffelClassesInCluster")
 324         env.AddMethod(spec_path, "EiffelSpecPath")
 325 
 326         for v in ['ISE_EIFFEL', 'ISE_PLATFORM', 'ISE_C_COMPILER']:
 327                 if not environment_variable(env, v):
 328                         print '****** WARNING! Undefined Eiffel environment variable ' + v + '.'
 329 
 330 def exists(env):
 331         """Is the Eiffel compiler available?"""
 332         return env.Detect(env['EC'])
 333 
 334 # Utility functions.
 335 
 336 def environment_variable(env, var):
 337         """
 338         The value of the environment variable 'var' within 'env'.
 339         If undefined and it is one of the standard EiffelStudio variables, a sensible platform-specific assumption is used; else None.
 340         """
 341         result = None
 342         var = var.lstrip('$').lstrip('(').rstrip(')')
 343 
 344         if env['ENV'].has_key(var):
 345                 result = env['ENV'][var]
 346         elif var == 'ISE_PLATFORM':
 347                 if env['PLATFORM'] == 'win32':
 348                         result = 'windows'
 349                 elif env['PLATFORM'] == 'darwin':
 350                         result = 'macosx-x86'
 351                 else:
 352                         result = 'linux-x86'
 353         elif var == 'ISE_C_COMPILER':
 354                 if env['PLATFORM'] == 'win32':
 355                         result = 'msc'
 356                 else:
 357                         result = 'gcc'
 358         elif var == 'ISE_EIFFEL':
 359                 if env.has_key('EC'): result = env.WhereIs(env['EC'])
 360                 if result: result = os.path.abspath(dirname(result, 5))
 361         elif var == 'ISE_LIBRARY':
 362                 result = environment_variable(env, 'ISE_EIFFEL')
 363 
 364         return result
 365 
 366 def classes_in_cluster(env, cluster):
 367         """All Eiffel class files in the given cluster and its subclusters."""
 368         result = []
 369 
 370         for root, dirnames, filenames in os.walk(cluster):
 371                 if '.svn' in dirnames: dirnames.remove('.svn')
 372                 result += env.Glob(root + '/*.e')
 373 
 374         return result
 375 
 376 def spec_path(env, mid_part, tail):
 377         """
 378         A platform-dependent path in the EiffelStudio installation directory of the form:
 379         $ISE_EIFFEL + mid_part + '/spec/' + $ISE_PLATFORM + '/' + tail
 380         If either of these environment variables is undefined, then the result is an empty string.
 381         """
 382         result = ''
 383         ise_eiffel = environment_variable(env, 'ISE_EIFFEL')
 384         ise_platform = environment_variable(env, 'ISE_PLATFORM')
 385 
 386         if ise_eiffel and ise_platform:
 387                 result = os.path.join(ise_eiffel, mid_part)
 388                 result = os.path.join(result, 'spec')
 389                 result = os.path.join(result, ise_platform)
 390                 result = os.path.join(result, tail)
 391                 result = os.path.abspath(result)
 392 
 393         return result
 394 
 395 def dirname(path, n):
 396         """The directory name of 'path', called recursively 'n' times."""
 397         result = path
 398         if n > 0: result = dirname(os.path.dirname(path), n - 1)
 399         return result

Possible Enhancements

The builder should define ECCOM and ECCOMSTR to support that common idiom.

On Windows, if one of the ISE_* environment variables is not defined then EiffelStudio looks for it in the registry. The Scanner could simulate this better by likewise looking at the registry. The trouble with this approach (apart from the added complexity) is that it might easily break if EiffelStudio's use of the registry changes in future versions.

When doing a .NET build, the Scanner prints a warning that $ISE_DOTNET_FRAMEWORK is undefined. This is the directory containing .NET framework assemblies. We would not normally expect the environment variable to be defined, so it would be good to suppress the warning, unless a reliable way can be found to guess at its value.

The Scanner scans all of the dependencies that the .ecf mentions, blindly ignoring any targets or conditions specified in the .ecf. This can cause unnecessary nodes in the dependency list.

EiffelStudioTool (last edited 2013-08-28 20:40:23 by WilliamDeegan)