Introduction
Navid has been busy setting up a demonstration environment using SCons and the ScienceTools v8r2.
Location of Installation
A new version of SCons has been installed since the previous version had a bug with swig and variant builds.
SCons is installed here at SLAC: /afs/slac/g/glast/applications/SCons/0.97.0d20070809/bin/scons
There's a copy of SciTools in Navid's area: /nfs/farm/g/glast/u06/golpa/ST-v8r2-scons/ScienceTools
If you desire to run SCons, please read below on how to check it out from the CVS copy.
CVS
The CVS copy is located in /nfs/farm/g/glast/u33/golpa/cvs
. To check out the code set your CVSROOT environment variable to point to that location. Once that is done you can check out a v8r2 version of ScienceTools by issuing the command cvs co -r ScienceTools-v8r2 ScienceTools-scons
.
To allow CVS to do such checkouts while staying compatible with CMT, the structure of CVS has been changed as follows:
CVSROOT | +--ScienceTools/ | +--astro/ | | | +--cmt/ | | | +--src/ | | | +--... +--... | +--ScienceTools-scons | +--astro/ --> Symlink back to above package | +--Likelihood/ --> Symlink back to above package | +--... | +--SConsctruct | +--externals.scons | +--site_scons/ | +--site_tools/ | +--addLibrary.py | +--registerObjects.py
Basic SCons commands
- To see a list of available command line options type
/afs/slac/g/glast/applications/SCons/0.97.0d20070809/bin/scons -h
. - To do a build of all the libraries and the main applications type
/afs/slac/g/glast/applications/SCons/0.97.0d20070809/bin/scons with-GLAST-EXT=/path/to/GLAST_EXT
. - To build only a specific package (and all the libraries it depends on) type the previous command adding the name of the package(s) to the end.
- To build everything above and test applications type the previous command but using the package name
all
.
Interesting Details and things we may need to discuss
There is a top-level SConstruct file which contains configuration/build information to be used "globally". This includes options for the compilation such as debug and optimization flags and contains code to find the SConscript files part of this build. See the Gory Details of the SConstruct file for more information. There is another file at the top-level called externals.scons which is used to set up the location of the external libraries. More on this point further down in the External Libraries section.
Location of SCons specific files and package structure
Each package contains its own SConscript file which act as SCons' version of CMT's requirements file. You can see an example for the facilities package further down on this page. The SConstruct file communicates the location of the SConscript file for each package.
The virjpk subdirectories are not in place or assumed in this organization. There is a note out to Riccardo to check if that arrangement is acceptable to MRStudio. Doing without the package-tag subdirectories allows us to utilize CVS directly for our checkouts, rather than devising a way to recreate CMT checkouts.
External Libraries
Code in the SConstruct file provides a mechanism similar to automake that allows users to specify the location of external libraries and their headers using with-LIBRARY
, with-LIBRARY-lib
and with-LIBRARY-include
.
There is also an option to specify GLAST_EXT style directory layout by specifying the command line option with-GLAST-EXT
. Any of the libraries found in the GLAST_EXT style layout can still be overridden with the with-LIBRARY[-lib | -include]
options.
The code that provides the above functionality is located in externals.scons
. It is responsible for locating the third party libraries and adding appropriate -L options to the compile environment. It also adds to the compile environment a list of libraries that should be linked against when a certain third party library is required.
Variant Builds
To allow for building the same code with different compile options, I have enabled SCons variant builds. What this means is that when a compile is performed, SCons will create a "virtual" copy of the code in a subdirectory. It will perform all builds in that directory. The directory name that is used can be modified by specifying the variant=
option at the command line. If this option is not specified, SCons will determine the the OS it is running on and use that as the variant name. Additionally, it will determine if debug and/or optimized is enabled. If that is the case, it will add a -debug
and/or -optimized
to the variant name. Once the variant name is determined, builds are performed in [packageName]/build/[variant]
. Once compiled in there, the libraries and other platform dependent data is installed in subdirectories that contain this variant name. For example if a build is done without specifying the variant=
option on a Linux OS then the astro package would be built in astro/build/Linux
. Once everything is compiled, the libraries would be installed in lib/Linux/
, and the binaries would be in bin/Linux
. Header files, on the other hand, would be located in include/
.
Overriding packages
There are two ways two override packages. The first method you are doing a full build in your own environment with multiple versions of the same package. In the second method you are building only a few packages (again with potentially multiple versions) against an already built build version of the application.
- Doing a full build with multiple versions of the packages.
With this method there's nothing "special" you need to do. As long as your packages have the format packageName-distinctionString, then SCons will pick up only one version of this package. For example let's say you wish to compile ST-v8r2 with multiple versions of astro. You would check out ST-v8r2 as before which will check out the standard version of astro. Now you wish to compile a different version of astro. You simply check out the version of astro into a directory with slightly modified name that follows the convention above. For example you check it out as astro-1. When SCons sees this package it automatically knows that it's a different version of astro and compiles astro-1 instead of astro. If you have multiple several versions (for example astro, astro-1, astro-2,etc.), then SCons will pick the last one in lexical order (in this case astro-2). - Building a partial set of packages against a full build
In this case you run SCons with the argument override=somePath where somePath is the full path to the override directory. For example let's say the full build is located in /ST-v8r2 and you have overridden astro in ~/ST-v8r2-override then, while located in /ST-v8r2, you execute scons with-GLAST-EXT=/path/to/glast-ext override=~/ST-v8r2-override. This will build the new astro in ~/ST-v8r2-override against other libraries located in /ST-v8r2. Of course ~/ST-v8r2-override can contain multiple packages including multiple versions of the same package. In the latter case the same rules as the previous option apply.
In the likely event you don't wish to be located in /ST-v8r2 when compiling the override packages you can also do this build by adding an additional argument to scons. This argument is -C /ST-v8r2. This will tell SCons to change dir to /ST-v8r2 before executing and return back to your current dir after execution.
Gory Details of the SConstruct file
The SConstruct filfe begins by setting up some global variables. It then goes on and sets up the basic compile environment used by all packages to compile code.
import os,platform,SCons,glob,re platform = platform.system() prefix = '/usr/local/bin' override = '.' ######################### # Global Environment # ######################### baseEnv=Environment() if ARGUMENTS.get('debug',0): baseEnv.Append(CCFLAGS = "-g") platform+="-Debug" if ARGUMENTS.get('optimized',0): baseEnv.Append(CCFLAGS = "-O2") platform+="-Optimized" if ARGUMENTS.get('CC',0): baseEnv.Replace(CC = ARGUMENTS.get('CC')) if ARGUMENTS.get('CXX',0): baseEnv.Replace(CXX = ARGUMENTS.get('CXX')) if ARGUMENTS.get('CCFLAGS',0): baseEnv.Append(CCFLAGS = ARGUMENTS.get('CCFLAGS')) if ARGUMENTS.get('CXXFLAGS',0): baseEnv.Append(CXXFLAGS = ARGUMENTS.get('CXXFLAGS')) if ARGUMENTS.get('variant',0): platform = ARGUMENTS.get('Variant') if ARGUMENTS.get('prefix',0): prefix=ARGUMENTS.get('prefix') if ARGUMENTS.get('override',0): override=ARGUMENTS.get('override') SConsignFile(os.path.join(override,'.sconsign.dblite'))
It then continues on to set up some help strings that will be displayed by SCons to the user when requesting help on compiling.
helpString = """ Usage: scons [target] [compile options] Targets: Default: Build release binaries and libraries test: Build test binaries and required libraries binaries: Build release binaries and required libraries libraries: Build all libraries install: Install release binaries, includes, and libraries isntall-test: Install test binaries Compile Options: optimized: Set to 1 to compile with optimization. Default 0. debug: Set to 1 to compile with debug. Default 0. CC: Set to the compiler to use for C source code. CXX: Set to the compiler to use for C++ source code. CCFLAGS: Set to additional flags passed to the C compiler. CXXFLAGS: Set to additional flags passed to the C++ compiler. variant: Compile binaries into subdirectory pointed by this varia$ override: Directory where overridden packages are located. Relativ$ """ Export('baseEnv')
After that, the project environment is updated to include the location where libraries/binaries/includes/etc. will be both after compiling and later when asked to officially install the code into its final location (aka make install):
######################### # Project Environment # ######################### baseEnv.Append(LIBDIR = os.path.join(os.path.abspath(override),'lib',platform)) baseEnv.Append(BINDIR = os.path.join(os.path.abspath(override),'bin',platform)) baseEnv.Append(INCDIR = os.path.join(os.path.abspath(override),'include')) baseEnv.Append(PFILESDIR = os.path.join(os.path.abspath(override),'pfiles')) baseEnv.Append(TESTDIR = baseEnv['BINDIR']) baseEnv.Append(INSTBIN = os.path.join(prefix,'bin',platform)) baseEnv.Append(INSTLIB = os.path.join(prefix,'lib',platform)) baseEnv.Append(INSTINC = os.path.join(prefix,'include')) baseEnv.Append(INSTPFILES = os.path.join(prefix,'pfiles')) baseEnv.Append(INSTTEST = baseEnv['TESTDIR']) baseEnv.Append(CPPPATH = ['.']) baseEnv.Append(CPPPATH = ['src']) baseEnv.Append(CPPPATH = [baseEnv['INCDIR']]) baseEnv.Append(LIBPATH = [baseEnv['LIBDIR']]) baseEnv.AppendUnique(CPPPATH = os.path.join(os.path.abspath('.'),'include')) baseEnv.AppendUnique(LIBPATH = os.path.join(os.path.abspath('.'),'lib',platform))
Next, it sets up the external libraries and adds the external libraries command line options to the help string displayed to users and register that help string with SCons.
######################### # External Libraries # ######################### helpString += SConscript('externals.scons') helpString += """ Install Options: prefix: Location to install files ($PREFIX/bin, $PREFIX/include, etc.). Default: /usr/local """ Help(helpString)
The SConstruct file then defines a function responsible for listing files in the "virtual" directory for the variant build.
def listFiles(files): allFiles = [] for file in files: if file.find('*') == -1: allFiles.append(file) else: newFiles = glob.glob(os.path.join(str(Dir('.').srcnode()),file)) for newFile in newFiles: newFile = str(Dir('.').srcnode().rel_path(File(newFile))) allFiles.append(newFile) return allFiles Export('listFiles')
A check is then performed to see if the target being performed is the install target. If that's the case, additional files are registered to be installed
if 'install' in COMMAND_LINE_TARGETS: baseEnv.NoClean(baseEnv.Default(baseEnv.Alias('install', baseEnv.Install(prefix, 'SConstruct')))) baseEnv.NoClean(baseEnv.Default(baseEnv.Alias('install', baseEnv.Install(prefix, 'externals.scons')))) baseEnv.NoClean(baseEnv.Default(baseEnv.Alias('install', baseEnv.Install(os.path.join(prefix, 'site_scons', 'site_tools'), os.path.join('site_scons','site_tools', 'registerObjects.py'))))) baseEnv.NoClean(baseEnv.Default(baseEnv.Alias('install', baseEnv.Install(os.path.join(prefix, 'site_scons', 'site_tools'), os.path.join('site_scons','site_tools', 'addLibrary.py'))))) baseEnv.NoClean(baseEnv.Default(baseEnv.Alias('install', baseEnv.Install(os.path.join(prefix,'bin',platform), glob.glob(os.path.join(baseEnv['BINDIR'],'*')))))) baseEnv.NoClean(baseEnv.Default(baseEnv.Alias('install', baseEnv.Install(os.path.join(prefix,'lib',platform), glob.glob(os.path.join(baseEnv['LIBDIR'],'*')))))) baseEnv.NoClean(baseEnv.Default(baseEnv.Alias('install', baseEnv.Install(os.path.join(prefix,'include'), glob.glob(os.path.join(baseEnv['INCDIR'], '*')))))) baseEnv.NoClean(baseEnv.Default(baseEnv.Alias('install', baseEnv.Install(os.path.join(prefix,'pfiles'), glob.glob(os.path.join(baseEnv['PFILESDIR'], '*'))))))
Towards the end, a the SConstruct file attempts to locate all SConscript files and all tools. It regsiteres all the tools with SCons and stores the location of the SConscript files in a list. Additional logic checks to see if a package is listed twice. If that's the case the package that is lexically last is only added. The other versions of the package are ignored.
directories = [override] packages = [] # Add packages to package list and add packages to tool path if they have one while len(directories)>0: directory = directories.pop(0) listed = os.listdir(directory) listed.sort() pruned = [] #Remove duplicate packages while len(listed)>0: curDir = listed.pop(0) package = re.compile('-.*$').sub('', curDir) while len(listed)>0 and re.match(package+'-.*', listed[0]): curDir = listed.pop(0) pruned.append(curDir) #Check if they contain SConscript and tools for name in pruned: package = re.compile('-.*$').sub('',name) if name != 'build': fullpath = os.path.join(directory,name) if os.path.isdir(fullpath): directories.append(fullpath) if os.path.isfile(os.path.join(fullpath,"SConscript")): packages.append(fullpath) if os.path.isfile(os.path.join(fullpath, package+'Lib.py')): SCons.Tool.DefaultToolpath.append(os.path.abspath(fullpath)) if 'install' in COMMAND_LINE_TARGETS: baseEnv.NoClean(baseEnv.Default(baseEnv.Alias('install', baseEnv.Install(os.path.join(prefix,'site_scons','site_tools'), os.path.join(fullpath,package+'Lib.py')))))
Finally, if the target being build is not the install target, SCons is told to load all SConscript files one at a time and control is handed over to SCons to start the build process.
if 'install' not in COMMAND_LINE_TARGETS: for pkg in packages: baseEnv.SConscript(os.path.join(pkg,"SConscript"), build_dir = os.path.join(pkg, 'build', platform))
An Example SConscript File
This is an example of the structure of a SConscript file. This example is from the fitsGen package. First some variables and objects are imported into the SConscript file. The import
command is a python command while the Import()
command is an SCons command importing variables exported previously by an Export()
command.
import glob,os,platform Import('baseEnv') Import('listFiles')
Next the basic environment is cloned for local use
progEnv = baseEnv.Clone() libEnv = baseEnv.Clone()
It is important that the environment is cloned before changes are made to it. If that is not done, any changes will propagate to all other packages.
Optionally, if there are changes that need to be made to the environment the can be done. In this case a CPP define is added to the compile command if the OS is a Linux system
if platform.system() == 'Linux': progEnv.Append(CPPDEFINES = 'TRAP_FPE')
Now, a static library with the name fitsGen
is created that consists of the files in src/*.cxx
.
The library is created in the libEnv
environment. This is because the progEnv
environment adds additional link commands to linker that are not needed for a library and will in fact cause circular dependency if specified.
fitsGenLib = libEnv.StaticLibrary('fitsGen', listFiles(['src/*.cxx']))
After telling SCons to create a library, we specify that progEnv
environment should add the fitsGen library to be linked in when linking the applications. All libraries the fitsGen library depends on are included as well.
This time we use the progEnv environment so that the previous environment does not link against fitsGen and its dependencies
progEnv.Tool('fitsGenLib') makeFT1Bin = progEnv.Program('makeFT1', 'src/makeFT1/makeFT1.cxx') makeFT2Bin = progEnv.Program('makeFT2', 'src/makeFT2/makeFT2.cxx') makeFT2aBin = progEnv.Program('makeFT2a', 'src/makeFT2a/makeFT2a.cxx') egret2FT1Bin = progEnv.Program('egret2FT1', listFiles(['src/egret2FT1/*.cxx'])) convertFT1Bin = progEnv.Program('convertFT1', 'src/convertFT1/convertFT1.cxx') partitionBin = progEnv.Program('partition', 'src/partition/partition.cxx') irfTupleBin = progEnv.Program('irfTuple', listFiles(['src/irfTuple/*.cxx']))
Finally, we register all these objects so that they can be assigned to the correct targets to be built and that their final locations are computed.
progEnv.Tool('registerObjects', package = 'fitsGen', libraries = [fitsGenLib], binaries = [makeFT1Bin, makeFT2Bin, makeFT2aBin, egret2FT1Bin, convertFT1Bin, partitionBin, irfTupleBin], includes = listFiles(['fitsGen/*.h']), pfiles = listFiles(['pfiles/*.par']))
Dependency Computation
SCons uses something called tools. Tools are nothing more than python functions that modify the compile environment. This feature was recommended by SCons developers to use for calculating dependencies recursively. To this this a package creates a file that is [packageName]Lib.py
. This file contains two functions generate()
and exists()
. The generate()
function does the actual modifications of the environment while the exists()
function is to give an option to disable the tool under certain conditions. Here's an example from the Likelihood package.
The LikelihoodLib.py file first defines the generate()
function. This function uses the addLibrary tool to add its own libraries to the link command. It then calls the tool for other packages it directly depends on. It also calls the addLibrary
tool to register external libraries it depends on.
It is important that only libraries that are needed for the library being added are listed here. For example liblikelihood.[so|a] directly depends on astro lib. The astro library should therefore be called. Likelihood test applications might also depend on cppunit external library but they should not be listed here since they are not needed for linking against liblikelihood.[so|a].
def generate(env, **kw): env.Tool('addLibrary', library=['Likelihood'], package = 'Likelihood') env.Tool('astroLib') env.Tool('xmlBaseLib') env.Tool('tipLib') env.Tool('evtbinLib') env.Tool('map_toolsLib') env.Tool('optimizersLib') env.Tool('irfLoaderLib') env.Tool('st_facilitiesLib') env.Tool('dataSubselectorLib') env.Tool('hoopsLib') env.Tool('st_appLib') env.Tool('st_graphLib') env.Tool('addLibrary', library=env['cfitsioLibs']) env.Tool('addLibrary', library=env['fftwLibs'])
Finally, the exists()
function is created which does nothing more than return a true value to notify SCons that this tool is enabled.
def exists(env): return 1
Interesting Python and SCons filesystem manipulation routines
os.path.join joins 'src' and '*.cxx' together in the correct way ie \ for windows and / for linux
glob.glob then takes the src/*.cxx and gets a list of files that match that pattern
I have added a listFiles() function that'll list the files in the "virtual" environment that SCons creates.
Environment variables
Our use of environment variables set by CMT needs to be removed. All use of [PACKAGENAME]ROOT environment variables is being removed in favor of using commonUtilities functions such as getDataPath(). Some other variables are, however, being used in parts of the code. These need to be addressed separately.
Name |
Status |
Solution |
Used In |
---|---|---|---|
CALDB |
Needed |
Set at runtime using commonUtilities::setEnvironment() |
irfs/caldb, irfs/dc1Response, irfs/dc1aResponse, irfs/irfUtil |
CALDBCONFIG |
Needed |
Set at runtime using commonUtilities::setEnvironment() |
irfs/caldb, irfs/irfUtil |
CALDBALIAS |
Needed |
Set at runtime using commonUtilities::setEnvironment() |
irfs/caldb |
PULSAROUTFILES |
Not Needed |
Obtain log directory using commonUtilities::getLogPath() |
celestialSources/Pulsar |
SKYMODEL_DIR |
Not Needed |
Remove use of this variable |
celestialSources/Pulsar, Gleam |
PULSARDATA |
Not Needed |
Use getDataPath("Pulsar") instead |
celestialSources/Pulsar, Gleam |
BYPASS_ACCUMULATOR |
Unknown |
|
Likelihood |
USE_OLD_LOGLIKE |
Unknown |
|
Likelihood |
HANDOFF_IRF_NAME |
Unknown |
|
irfs/handoff_response |
INTERPOLATE_EDISP |
Unknown |
|
irfs/handoff_response |
CALIB_DIR |
Unknown |
|
irfs/irfLoader |
OPTIMIZERSROOT |
Unknown |
Leave as is. It's written out to an xml file |
optimizers |
TIMING_DIR |
Unknwon |
Replace with EXTFILESSYS |
timeSystem |
EXTFILESSYS |
Needed |
Set at runtime using setupEnvironment() |
various packages |
MERIT_INPUT_FILE |
Unknown |
Unknown |
GlastClassify, merit |
MERIT_OUTPUT_FILE |
Unknown |
Unknown |
GlastClassify, merit |
CTREE_PATH |
Unknown |
Unknown |
GlastClassify |
PRUNEROWS |
Unknown |
Unknown |
GlastClassify |
Fred_DIR |
Not Needed |
Use $GLAST_EXT/Fred/<version> |
Gleam |
SKYMODEL_DIR |
Unknown |
Unknown |
Gleam |
POINTING_HISTORY_FILE |
Unknown |
Unknown |
Gleam |
MOOT_ARCHIVE |
Unknown |
Unknown |
MootSvc, configData |
OBFXFCBINDIR |
Unknown |
Unknown |
OnboardFilter |
OBFXFC_DBBINDIR |
Unknown |
Unknown |
OnboardFilter |
OBFEFCBINDIR |
Unknown |
Unknown |
OnboardFilter |
OBFGFC_DBBINDIR |
Unknown |
Unknown |
OnboardFilter |
OBFCPP_DBBINDIR |
Unknown |
Unknown |
OnboardFilter |
OBFCPG_DBBINDIR |
Unknown |
Unknown |
OnboardFilter |
OBFGGF_DBBINDIR |
Unknown |
Unknown |
OnboardFilter |
MYSQL_METATABLE |
Unknown |
Unknown |
calibUtil |
MYSQL_HOST |
Unknown |
Unknown |
calibUtil |
USER |
Unknown |
Unknown |
calibUtil, rdbModel |
TESTJOBOPTIONS |
Unknown |
Unknown |
gr_app |
GLEAM_CHDIR |
Unknown |
Unknown |
gr_app |
JOBOPTIONS |
Unknown |
Unknown |
gr_app |
stdout |
Unknown |
Unknown |
gui |
CTREE_PATH |
Unknown |
Unknown |
merit |
MOOT_ARCHIVE |
Unknown |
Unknown |
mootCore |
USERNAME |
Unknown |
Unknown |
rdbModel |
mycalibs |
Unknown |
Unknown |
rdbModel |