#!/usr/bin/python # -*- coding: utf-8 -*- """ SConstruct file - replacement for Makefile DESCRIPTION This file is used to compile, install and clean Cepam source code, libraries and binaries. It can also manage other tasks, as archiving, committing to svn server, manage versions... USAGE scons [OPTIONS] [TARGETS] OPTIONS -h -- show complete help and options -c -- clean the the current build AUTHOR Mathieu Havel (mathieu.havel@oca.eu) CREATION DATE (dd/mm/yyyy) 23/01/2009 LAST CHANGES (dd/mm/yyyy) 15/10/2015 -- improvements of SConstruct (more clarity) [MH] 05/03/2015 -- compiler options fixed [MH] 03/06/2014 -- several bugs fixed 10/06/2013 -- user-specified compiler is now better supported 04/03/2011 -- fixed symbolic link issues, and put all programs in bin/ (clean root dir) [MH] 24/09/2010 -- removed static, check and test now depend on optimise [TG] 10/10/2009 -- added test of static model (static option) [TG] 23/01/2009 -- creation LICENCE GPL """ # ================== # Python module # ================== import os import sys import atexit import cepam_versioning import cepam_env # ================ # Initial checks # ================ EnsureSConsVersion(1, 0) # ==================== # Version management # ==================== version = cepam_versioning.VersionNumber() # ======================= # Machine configuration # ======================= this = cepam_env.This() # ========================= # Compilation environment # ========================= env = Environment(FORTRANPATH=['#', this.mod_dir, this.src_dir], # search path ('#' means home of Cepam) ENV=os.environ) # OS specific settings conf = Configure(env) # configure some tools for Fortran # (configure the TAR archiver, even if not available) for _c in ('fortran', 'f90', 'f95', 'ifort', 'tar'): conf.env.Tool(_c) # 'ar' flags to create libraries env.Replace(ARFLAGS='ruc') # signature methods (to check if a file must be rebuild or not) Decider('MD5-timestamp') # use both "MD5" and "timestamp" # supported compilers available_compilers = ['gfortran-mp-5', 'gfortran', 'ifort', 'lf95'] # default compilation flags FFLAGS = '' LFLAGS = '' FMODDIRPREFIX = '' # automatic detection of Fortran compiler. # The first detected in the list will be used, so the default compiler is 'gfortran' # To force it, you can use 'compiler' argument in the scons command line. compiler = conf.env.Detect(available_compilers) _fc = ARGUMENTS.get('compiler', '') if _fc in available_compilers: compiler = _fc elif _fc: if any(map(lambda c: _fc.startswith(c), available_compilers)): # a special version of a supported compiler compiler = _fc else: print 'The given compiler is not supported. Do you want to use it anyway (yes/no)?' choice = sys.stdin.readline().rstrip('\n').strip() if choice in ('Y', 'y', 'Yes', 'yes', 'YES'): compiler = _fc print 'what are the compiler flags? (eg. "-fmax-errors=5 -std=legacy"):' FFLAGS += ' ' + sys.stdin.readline().rstrip('\n').strip() print 'what are the library flags? (eg. "-Wl,--subsystem,console -mwindows"):' LFLAGS += sys.stdin.readline().rstrip('\n').strip() print 'what is the fortran module prefix? (eg. "-J"):' FMODDIRPREFIX += sys.stdin.readline().rstrip('\n').strip() else: print 'Using the detected compiler: %s' % compiler env = conf.Finish() # compilation options optimize = 0 for _o in ('o', 'optimize', 'optimized'): if _o in ARGUMENTS: optimize = ARGUMENTS.get(_o) break parallelize = 0 for _o in ('p', 'parallelize', 'parallel', 'parallelized'): if _o in ARGUMENTS: parallelize = ARGUMENTS.get(_o) break # compilation flags setup if optimize: if parallelize: FFLAGS += ' -O3' else: FFLAGS += ' -O2' if compiler.startswith('gfortran'): # GNU Gfortran compiler # -march=i386 -ffpe-trap=invalid'#-fbounds-check # FFLAGS += ' -fmax-errors=5 -ffree-line-length-132 -std=legacy -fd-lines-as-comments' FFLAGS += ' -fmax-errors=5 -ffree-line-length-132 -std=legacy' elif compiler.startswith('ifort'): # Intel Fortran compiler FFLAGS += ' -error-limit 5 -132 -free' elif compiler.startswith('lf95'): # Lahey / Fujitsu Fortran(lf95) compiler FFLAGS += ' --maxfatals 5 --wide --tp4 --warn' # cache directory CacheDir(this.cache(compiler)) if compiler.startswith('gfortran'): env['INCPREFIX'] = '-B' if parallelize: FFLAGS += ' -ftree-vectorize -fopenmp' FMODDIRPREFIX = '-J' elif compiler.startswith('ifort'): if parallelize: FFLAGS += ' -parallel -mkl=parallel' FMODDIRPREFIX = '-module ' elif compiler.startswith('lf95'): if parallelize: FFLAGS += ' --parallel --ocl --threads 2' FMODDIRPREFIX = '--mod ' # ======================= # Windows configuration # ======================= if this.platform == 'win32': # installation directory (should be in Windows PATH and writable without root privilege) os_install_dir = 'C:\\MinGW\\bin' # library path os_lib_dir = ['C:\\MinGW\\lib'] if compiler.startswith('gfortran'): LFLAGS += ' -Wl,--subsystem,console -mwindows' elif compiler.startswith('ifort'): LFLAGS += ' -mwindows' elif compiler.startswith('lf95'): LFLAGS += ' -mwindows' # ==================================== # POSIX configuration (Linux + OS X) # ==================================== else: # installation directory os_install_dir = '/usr/local/bin' # library path os_lib_dir = ['/usr/local/lib', '/usr/lib'] if this.platform == 'darwin' and compiler.startswith('ifort'): LFLAGS += ' -headerpad_max_install_names' # =========== # Libraries # =========== lib_paths = os_lib_dir if os.path.isdir(this.lib_dir): lib_paths.append(this.lib_dir) # Uncomment the line below if needed (D. Reese's subroutines) # lib_list = ['blas', 'lapack', 'pthread'] lib_list = [] LFLAGS = (FFLAGS + ' ' + LFLAGS).rstrip() # =============================== # SCONS compilation environment # =============================== env.Replace(CC=compiler) # C compiler (fake ! CC should be equal to FC ) env.Replace(FORTRAN=compiler) # Fortran Compiler env.Replace(F77=compiler) # Fortran Compiler env.Replace(F90=compiler) # Fortran Compiler env.Replace(F95=compiler) # Fortran Compiler if FFLAGS: env.Replace(F77FLAGS=FFLAGS) # Fortran flags for compilation env.Replace(F90FLAGS=FFLAGS) # Fortran flags for compilation env.Replace(F95FLAGS=FFLAGS) # Fortran flags for compilation env.Replace(FORTRANFLAGS=FFLAGS) # Fortran flags for compilation env.Replace(FORTRANMODDIR=this.mod_dir) # "modules" directory if FMODDIRPREFIX: env.Replace(FORTRANMODDIRPREFIX=FMODDIRPREFIX) # "modules" prefix compilation if LFLAGS: env.Replace(LINKFLAGS=LFLAGS) # Fortran flags for linking env.SetDefault(F90PATH=env['FORTRANPATH']) env.SetDefault(F95PATH=env['FORTRANPATH']) # export compilation environment and paths for other SConscript Export('this env lib_list lib_paths') # ========= # Targets # ========= # Importing objects of CEPAM code objs = SConscript([this.sconscript('src_dir')]) # -- localize CEPAM -- def localize_cepam(target, source, env): """ Create a module holding the path to CEPAM source and data directory """ name = this.source('mod_cepam_installdir.f90', path='loc_dir') if not os.path.isdir(this.loc_dir): os.makedirs(this.loc_dir) if not os.path.isfile(name): with open(name, 'w') as f: f.write('\n'.join( ('! This is an automatically generated file containing the location of the CEPAM code files', 'MODULE mod_cepam_installdir', ' CHARACTER(LEN=80) :: installdir="%s"' % os.path.abspath(this.root), 'END MODULE mod_cepam_installdir') )) return local = env.Command('noneloc', '', localize_cepam) # -- CEPAM executable -- # default target ; installed in 'bin' under name 'cepam' objlaunch = env.Object(this.source('cepamlaunch.f')) cepam = env.Program(this.target('cepam'), objlaunch + objs, LIBS=lib_list, LIBPATH=lib_paths) env.Requires(cepam, local) Default(cepam) # -- optimize -- # Optimized version of Cepam for Jupiter and Saturn (based on observed gravitational moments) objoptimise = env.Object(this.source('optimise.f90')) optimise = env.Program(this.target('optimise'), objoptimise + objs, LIBS=lib_list, LIBPATH=lib_paths) env.Requires(optimise, local) # -- cepasc2bin -- # ASCII <-> BIN # objasc2bin = env.Object([this.source('cepasc2bin.f90'), # this.source('Listes', 'rwbin.f90'), # this.source('Maths', 'leng.f')]) # cepamasc2bin = env.Program(this.target('cepasc2bin'), objasc2bin, # LIBS=lib_list, LIBPATH=lib_paths) objasc2bin = env.Object(this.source('cepasc2bin.f90')) cepamasc2bin = env.Program(this.target('cepasc2bin'), objasc2bin + objs, LIBS=lib_list, LIBPATH=lib_paths) # -- a2b_EOS -- # reconstruction of EOS data (a2b_EOS) a2bEOS = env.Program(this.target('a2b_EOS'), this.source('a2b_EOS.f')) # Aliases # install: install all targets, building them if needed with necessary libraries install = env.Alias('install', [cepam, optimise, cepamasc2bin, a2bEOS]) # ================= # Tasks functions # ================= # Executing installation tasks, if needed def exe_a2b(target, source, env): """ Convert .a and .a2 into .b and .b2 if needed :param target: :param source: :param env: """ a2bcmd = str(source[0]) a2blist = ('He_spleen_pt.b', 'inter_spleen_pt.b', 'ppt_spleen_pt.b', 'He_spleen_pt.b2', 'inter_spleen_pt.b2') runa2b = False for ffile in os.listdir(Dir(this.data('EOS')).srcnode().abspath): ffname, ffext = os.path.splitext(ffile) if ffext == '.a': if (ffname + '.b' in a2blist) and (not os.path.exists(this.data('EOS', ffname + '.b'))): runa2b = True break elif ffext == '.a2': if (ffname + '.b2' in a2blist) and (not os.path.exists(this.data('EOS', ffname + '.b2'))): runa2b = True break if runa2b: env.Execute(a2bcmd) runa2bEOS = env.Command('nonea2b', this.target('a2b_EOS'), exe_a2b) env.Requires(runa2bEOS, a2bEOS) def create_link(target, source, env): """this functions create links if needed""" if this.platform == 'win32': print "[WARNING] You cannot create link on windows. Thus CEPAM might not work" else: lnkcmd = str(source[0]) lnsrc = os.path.realpath(this.data_dir) print "Creating 'data' links..." for tdir in this.test_cases: aptdir = this.test(tdir) for mydir in os.listdir(aptdir): thisdir = os.path.join(aptdir, mydir) if os.path.isdir(thisdir) and (not mydir.startswith('.')): lntarget = os.path.join(thisdir, 'data') test1 = not os.path.exists(lntarget) test2 = os.path.islink(lntarget) and (not os.path.realpath(lntarget) == lnsrc) if test1 or test2: env.Execute(lnkcmd + ' -s -f ' + lnsrc + ' ' + lntarget) print 'Link directory: "' + lntarget + '" to "' + lnsrc + '"' return makelnk = env.Command('nonelnk', '/bin/ln', create_link) env.Requires(cepam, [makelnk, runa2bEOS]) # ======= # Tests # ======= Export('objs') # this is built only when scons is called with "install", ".", or "tests" argument tests = SConscript([this.sconscript('srcdiv_dir')]) # Executing tests, if scons called with "test" argument (without the endding "s") cmd = this.source('test_cepam.bash', path='com_dir') testop = '' print_warning = False warning = "[WARNING] Impossible to run tests on this platform" if this.platform == 'win32': print_warning = True testop = env.Command('none', 'none', '') if 'check' in COMMAND_LINE_TARGETS: if print_warning: print warning else: testop = env.Command('none', cmd, "/bin/bash $SOURCE -s") elif 'test' in COMMAND_LINE_TARGETS: if print_warning: print warning else: testop = env.Command('none', cmd, "/bin/bash $SOURCE") AlwaysBuild(testop) env.Requires(testop, [cepam, optimise]) # Aliases env.Alias('test', [cepam, optimise, cepamasc2bin, testop]) env.Alias('run_test', [cepam, optimise, cepamasc2bin, testop]) env.Alias('run_tests', [cepam, optimise, cepamasc2bin, testop]) env.Alias('test_cepam', [cepam, optimise, cepamasc2bin, testop]) env.Alias('test_all', [cepam, optimise, cepamasc2bin, testop]) env.Alias('check', [cepam, optimise, cepamasc2bin, testop]) env.Alias("all", [install, tests]) # =============== # Scons options # =============== # -- DEBUG -- # if 'debug=1' is set at scons call time, then Cepam is built in 'debug' mode debug = ARGUMENTS.get('debug', 0) if int(debug): _ff = ' -g' _lf = ' -g' if compiler.startswith('ifort'): _ff += ' -debug -traceback -Wl,-no_pie' _lf += ' -debug -traceback -Wl,-no_pie' env.Append(FORTRANFLAGS=_ff) env.Append(LINKFLAGS=_lf) # -- INSTALL -- link = ARGUMENTS.get('link', 0) if int(link): if this.system == 'posix': for _file in os.listdir(Dir(this.bin_dir).srcnode().abspath): _sf = os.path.join(this.bin_dir, _file) _df = os.path.join(os_install_dir, _file) os.system('ln -s -f ' + _sf + ' ' + _df) print 'Link file: "' + _df + '" to "' + _sf + '"' Exit(0) elif this.platform == 'win32': for _file in os.listdir(Dir(this.bin_dir).srcnode().abspath): # getting file name (taking off any extension) and add ".bat" extension filename = os.path.splitext(_file)[0] + '.bat' # creating file (.bat file with path to executable) _sf = os.path.join(this.bin_dir, _file) _df = os.path.join(os_install_dir, _file) with open(os.path.join(os_install_dir, filename), 'w') as f: f.write(_sf) print ('Create .bat file in: "' + _df + '" which will launch "' + _sf + '"') Exit(0) # ================== # Archive creation # ================== # Tar BZ2 archive # if 'archive=1' is set at scons call time, then an archive of CEPAM project is built archive = ARGUMENTS.get('archive', 0) if int(archive): # update the date assert version.upgrade('date') date_tar = version.date.replace('/', '-') # directories and files to archive archive_list = [ this.src_dir, this.doc_dir, this.dat_dir, this.com_dir, 'VERSION', 'README', 'SConstruct', 'DATE', 'MODIFS', 'Makefile', 'graph', 'input', 'cepam_param.don', 'modifs_don', 'PERFORMANCES', '__init__.py', 'cepam_env.py', 'cepam_versioning.py', ] # create archive if this.platform == 'win32': # WINDOWS : "libarchive" must be installed in order to get it working ! env.Replace(TAR='bsdtar') ext = '.tar.bz2' env.Replace(TARFLAGS='-j -c --exclude=.* --exclude=*.o --exclude=cepam --exclude=*~ --exclude=*.mod') env.Replace(TARSUFFIX=ext) tarfile = env.Tar(os.path.join(this.cur_dir, 'cepam_v' + str(version) + '_' + date_tar + ext), archive_list) Default(None) # clear all default targets Default(tarfile) ######################## ### Manual upgrade ### ######################## upver = ARGUMENTS.get('upver', 'none') if upver != 'none': up_success = version.upgrade(upver) if up_success: Exit(0) else: print 'Failed to upgrade version/date !' Exit(1) # ============ # Scons HELP # ============ Help(""" ~~~~~~~~~~~~~~~~~~~~~ --- General help --- ~~~~~~~~~~~~~~~~~~~~~ Type: 'scons' -- to build only binaries 'scons .' -- to (re)build all (libraries, binaries..., if sources changed) 'scons [TARGETS]' -- to build only specified [TARGETS] 'scons tests' -- to build test programs 'scons install' -- to build all binaries (not libraries if already built) 'scons compiler=NAME' -- to build using the given compiler 'scons debug=1' -- to build the 'debug' version program 'scons o=1' -- to build the optimized version program optimize=1 optimized=1 'scons p=1' -- to build the parallel version of the program parallel=1 parallelize=1 parallelized=1 'scons test' -- to run all test programs run_test run_tests test_cepam test_all 'scons check' -- to run one test program 'scons -c .' -- to clean non protected files (ie. not libraries) Supported compilers (you can specify another one to force using it): %s """ % available_compilers) # Release specific help Help(""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --- RELEASE specific help --- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Type: 'scons upver=ACTION' to change the version of Cepam (MajNum.RelNum.MinNum) EXIT right after Available ACTION are: * major - change version incrementing MajNum * release - change version incrementing RelNum * minor - change version incrementing MinNum * date - change only the date """) # LINUX / OS X specific help if this.system == 'posix': Help(""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --- LINUX / OS X specific help --- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Type: 'scons -Q link=1' -- to create symbolic links in "%s" for Cepam's binaries (it may be necessary to run this with root privileges) EXIT right after 'scons -Q archive=1' -- to (re)build an archive of the current state of Cepam project EXIT right after """ % os_install_dir) # WINDOWS specific help if this.platform == 'win32': Help(""" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --- WINDOWS specific help --- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Type: 'scons -Q link=1' -- to create .bat file in "%s", and executing Cepam's binaries (it may be necessary to run this with root privileges) EXIT right after 'scons -Q archive=1' -- to (re)build an archive of the current state of Cepam project (you'll need "libarchive" package installed) EXIT right after """ % os_install_dir) ############## ### INFO ### ############## print '' print ' ===== Cepam : builder tool =====' print '' print '***************************************************' infos = [ ('System', this.system), ('Platform', this.platform), ('Current version of CEPAM', version.string), ('SVN revision', version.revision), ('Fortran compiler', env['FORTRAN']) ] if env['LINKFLAGS'] != env['FORTRANFLAGS']: infos.extend( (('Fortran options', env['FORTRANFLAGS'].strip()), ('Linker flags', env['LINKFLAGS'].strip())) ) else: infos.append(('Fortran flags', env['FORTRANFLAGS'].strip())) if lib_list: infos.append(('Libraries used', lib_list)) print '\n'.join( map(lambda _s: '{:.<35} {:}'.format(_s[0] + ' ', _s[1]), infos) ) print '***************************************************' print '' print 'The following targets are going to be built :', map(str, BUILD_TARGETS) print '' # ======================= # Errors / build checks # ======================= def bf_to_str(bf): """Convert an element of GetBuildFailures() to a string in a useful way.""" import SCons.Errors if bf is None: # unknown targets product None in list return '(unknown tgt)' elif isinstance(bf, SCons.Errors.StopError): return str(bf) elif bf.node: return str(bf.node) + ': ' + bf.errstr elif bf.filename: return bf.filename + ': ' + bf.errstr return 'unknown failure: ' + bf.errstr def build_status(): """Convert the build status to a 2-tuple, (status, msg).""" from SCons.Script import GetBuildFailures bf = GetBuildFailures() if bf: # bf is normally a list of build failures; if an element is None, # it's because of a target that scons doesn't know anything about. status = 'failed' failures_message = "\n".join(["Failed building %s" % bf_to_str(x) for x in bf if x is not None]) else: # if bf is None, the build completed successfully. status = 'ok' failures_message = '' return status, failures_message def display_build_status(): """Display the build status. Called by atexit. Here you could do all kinds of complicated things.""" status, failures_message = build_status() if status == 'failed': print '***************************************************' print 'System is ', this.system print 'Platform is ', this.platform print 'Current version of CEPAM ', str(version) + ' (' + version.date + ')' print 'SVN revision ', version.revision print '***************************************************' print "FAILED!!!!" # could display alert, ring bell, etc. elif status == 'ok': if this.platform == 'darwin' and compiler.startswith('ifort'): print 'Updating OS X libraries path on executables...' _targets = set(map(str, BUILD_TARGETS)) for alias in ('install', 'test', 'check', 'all'): if alias in _targets: _targets.discard(alias) _targets.update(('cepam', 'optimise', 'cepasc2bin', 'a2b_EOS')) mklroot = os.path.join(os.getenv('MKLROOT', ''), 'lib') comroot = mklroot.replace('mkl', 'compiler') for _libname in ('iomp5', 'mkl_intel_lp64', 'mkl_core', 'mkl_intel_thread'): _lname = 'lib' + _libname + '.dylib' if _libname.startswith('mkl_'): _libpath = os.path.join(mklroot, _lname) else: _libpath = os.path.join(comroot, _lname) for _t in _targets: if os.path.isfile(_t): os.system(r'install_name_tool -change %s %s %s' % (_lname, _libpath, this.target(_t.lstrip('/bin')))) print "Build succeeded." print failures_message atexit.register(display_build_status)