source: trunk/fsource/SConstruct @ 5369

Last change on this file since 5369 was 5369, checked in by toby, 7 months ago

Incorporate James Hester's updates for high angle FCJ

File size: 20.5 KB
Line 
1# this is a build script that is intended to be run from scons (it will not work in python)
2# it compiles the Fortran files needed to be used as Python packages for GSAS-II
3#
4# if the default options need to be overridden for this to work on your system, please let us
5# know the details of what OS, compiler and python you are using as well as the command-line
6# options you need.
7from __future__ import division, print_function
8import platform
9import sys
10import os
11import glob
12import subprocess
13#==========================================================================================
14def is_exe(fpath):
15    return os.path.exists(fpath) and os.access(fpath, os.X_OK)
16def which_path(program):
17    "emulates Unix which: finds a program in path, but returns the path"
18    import os, sys
19    if sys.platform == "win32" and os.path.splitext(program)[1].lower() != '.exe':
20        program = program + '.exe'
21    fpath, fname = os.path.split(program)
22    if fpath:
23        if is_exe(program):
24            return fpath
25    else:
26        for path in os.environ["PATH"].split(os.pathsep):
27            exe_file = os.path.join(path, program)
28            if is_exe(exe_file):
29                return path
30    return ""
31
32def GetBinaryDir():
33    '''Format current platform, Python & numpy version;
34    note that this must match GSASIIpath.GetBinaryPrefix
35    '''
36    if sys.platform == "win32":
37        prefix = 'win'
38    elif sys.platform == "darwin":
39        prefix = 'mac'
40    elif sys.platform.startswith("linux"):
41        prefix = 'linux'
42    else:
43        print(u'Unknown platform: '+sys.platform)
44        raise Exception('Unknown platform')
45    if 'arm' in platform.machine() and sys.platform == "darwin":
46        bits = 'arm'
47    elif 'aarch' in platform.machine() and '64' in platform.architecture()[0]:
48        bits = 'arm64'
49    elif 'arm' in platform.machine():
50        bits = 'arm32'
51    elif '64' in platform.architecture()[0]:
52        bits = '64'
53    else:
54        bits = '32'
55    pyver = 'p{}.{}'.format(pyVersions[0].strip(),pyVersions[1].strip())
56    #pyver = 'p{}.{}'.format(*sys.version_info[0:2])
57    npver = 'n' + pyVersions[3][:pyVersions[3].find('.',2)]
58    return '_'.join([prefix,bits,pyver,npver])
59#==========================================================================================
60# misc initializations
61# need command-line options for fortran command and fortran options
62F2PYflags = '' # compiler options for f2py command
63if 'arm' in platform.machine() and sys.platform == "darwin":
64    F2PYflags = '-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/usr/include' # compiler options for f2py command
65F2PYpath = ARGUMENTS.get('F2PYpath', '')
66# find a default f2py relative to the scons location. Should be in the same place as f2py
67spath = os.path.normpath(os.path.split(sys.executable)[0])
68for pth in [F2PYpath,spath,os.path.normpath(os.path.join(spath,'..')),os.path.join(spath,'Scripts')]:
69    if not pth: continue
70    # look for f2py3 first
71    if sys.platform == "win32":
72        program = 'f2py3.exe'
73    else:
74        program = 'f2py3'
75    f2pyprogram = os.path.join(pth,program)
76    if is_exe(f2pyprogram):
77        F2PYpath,F2PYprog = os.path.split(f2pyprogram)
78        break
79    # not there, try f2py
80    if sys.platform == "win32":
81        program = 'f2py.exe'
82    else:
83        program = 'f2py'
84    f2pyprogram = os.path.join(pth,program)
85    if is_exe(f2pyprogram):
86        F2PYpath,F2PYprog = os.path.split(f2pyprogram)
87        break
88    # none of the above, look for f2py.py (probably obsolete)
89    program = 'f2py.py'
90    f2pyprogram = os.path.join(pth,program)
91    if os.path.exists(f2pyprogram) and os.path.splitext(program)[1] == '.py':
92        F2PYpath,F2PYprog = os.path.split(f2pyprogram)
93        break
94else:
95    print ('Note: Using f2py from path (hope that works!)')
96    F2PYpath = which_path('f2py')       # default path to f2py
97    F2PYprog = 'f2py'
98# check if we have a working path to f2py:
99f2pyprogram = os.path.normpath(os.path.join(F2PYpath,F2PYprog))
100if os.path.exists(f2pyprogram) and os.path.splitext(program)[1] == '.py':
101    pass
102elif is_exe(f2pyprogram):
103    pass
104else:
105    print ('''
106ERROR: The f2py program was not found. If this program is installed
107but not in your path, you should specify the path on the command line:
108   scons -Q F2PYpath=/Library/Frameworks/Python.framework/Versions/6.2/bin/
109   scons -Q F2PYpath=D:/Python27/Scripts
110''')
111    sys.exit()
112
113GFORTpath = which_path('gfortran')   # path to compiler
114FCompiler='gfortran'
115G77path = which_path('g77')     # path to compiler
116FORTpath = ""
117FORTflags = ""
118LDFLAGS = ''
119SDKROOT = ''
120tmpdir = None
121#==========================================================================================
122EXEsuffix = ''
123# configure platform dependent options here:
124if sys.platform == "win32":
125    F2PYsuffix = '.pyd'
126    EXEsuffix = '.exe'
127    if G77path != "":
128      FCompiler='g77'
129    elif GFORTpath != "":
130      FCompiler='gfortran'
131    else:
132      print ('No Fortran compiler in path')
133      sys.exit()
134elif sys.platform == "darwin":
135    if 'arm' in platform.machine():
136        LDFLAGS = '-undefined dynamic_lookup -bundle -rpath ./ -mmacosx-version-min=11.1'
137        # I am not sure I completely understand the -rpath & -mmacosx-version-min options,
138        # but thet seem to work; this probably needs to be tested more
139    else:
140        LDFLAGS = '-undefined dynamic_lookup -bundle'
141    SDKROOT = os.environ.get('SDKROOT','')
142    F2PYsuffix = '.so'
143elif sys.platform.startswith("linux"):
144    #LDFLAGS = '-Wall -shared -static-libgfortran -static-libgcc' # does not work with gfortran 4.4.4 20100726 (Red Hat 4.4.4-13)
145    F2PYsuffix = '.so'
146else:
147    print ("Sorry, parameters for platform "+sys.platform+" are not yet defined")
148    sys.exit()
149if ARGUMENTS.get('TMP'):
150   tmpdir = ARGUMENTS.get('TMP')
151if FCompiler == 'gfortran':
152    if ARGUMENTS.get('LIBGCC', '').upper().startswith('T'):
153        LDFLAGS += ' -static-libgcc'
154        print('LIBGCC')
155    if ARGUMENTS.get('LIBGFORTRAN', '').upper().startswith('T'):
156        LDFLAGS += ' -static-libgfortran'
157        print('LIBGfortran')
158   
159#==========================================================================================
160# help
161if 'help' in COMMAND_LINE_TARGETS:
162    print ("""
163----------------------------------------------
164Building Fortran routines for use with GSAS-II
165----------------------------------------------
166
167To build the compiled modules files needed to run GSAS-II, invoke this script:
168    scons [options]
169where the following options are defined (all are optional):
170
171-Q      -- produces less output from scons
172
173-n      -- causes scons to show but not execute the commands it will perform
174
175-c      -- clean: causes scons to delete previously created files (but not from
176   install directory)
177
178help    -- causes this message to be displayed (no compiling is done)
179
180install=T -- causes the module files to be placed in an installation directory
181   (../AllBinaries/<X>_NN_pv.v_nv.v) rather than ../bin, where:
182     <X> is mac, win or linux;
183     NN is 64 or 32 for Intel; arm64 or arm32 for ARM (aarch or arm)
184     pv.v is the Python version (p2.7 or p3.6,...) and
185     nv.v is the Numpy version (n1.13,...).
186   Normally used only by Brian or Bob for distribution of compiled software.
187
188The following options override defaults set in the scons script:
189
190FCompiler=<name>  -- define the name of the fortran compiler, typically g77
191   or gfortran; default is to use g77 on Windows and gfortran elsewhere. If
192   you use something other than these, you must also specify F2PYflags.
193
194FORTpath=<path>    -- define a path to the fortran program; default is to use
195   first gfortran (g77 for Windows) found in path
196
197FORTflags='string' -- string of options to be used for Fortran
198   during library build step
199
200F2PYpath=<path>    -- define a path to the f2py program; default is to use
201   first f2py found in path
202
203F2PYflags='string' -- defines optional flags to be supplied to f2py:
204   Typically these option define which fortran compiler to use.
205
206F2PYsuffix='.xxx'  -- extension for output module files (default: win: '.pyd',
207   mac/linux: '.so')
208
209LIBGCC=T -- adds the option -static-libgcc as an link option with gfortran. This
210   prevents use of the dynamic libgcc library, which must be then present on each
211   run-time computer. To use this, gfortran must be installed with the static
212   libraries.
213
214LIBGFORTRAN=T -- adds the option -static-libgfortran as an link option with
215   gfortran. This prevents use of the dynamic libgcc library, which then must be
216   present on each run-time computer. To use this, gfortran must be installed
217   with the static libraries.
218     
219LDFLAGS='string'   -- string of options to be used for f2py during link step
220
221TMP=<path> --- where <path> is something like /tmp sets builds to be performed
222   in that directory.
223
224Note that at present, this has been tested with 32-bit python on windows and
225Mac & 64 bit on linux. 32-bit builds with anaconda/gfortran in 32-bit Python
226is not working, at least not when installed in 64-bits Linux/Windows.
227
228examples:
229    scons -Q
230       (builds into ../bin for current platform using default options)
231    scons -Q install=t
232       (builds into ../bin<platform-dir> for module distribution)
233    """)
234    sys.exit()
235#==========================================================================================
236# override from command-line options
237for var in ['F2PYflags','F2PYpath','F2PYsuffix','FCompiler','FORTpath','FORTflags','LDFLAGS']:
238    if ARGUMENTS.get(var, None) is not None:
239        print ('Setting',var,'to',ARGUMENTS.get(var),'based on command line')
240        exec(var + "= ARGUMENTS.get('" + var +"')")
241#==========================================================================================
242# get the python version number from the python image in the f2py directory
243# find the python location associated with the f2py in use. Note
244#   that on Windows it may be in the parent of the f2py location.
245# then run it to get info about the verision and the number of bits
246pythonpath = ''
247for program in ['python3','../python3','python','../python']:
248    if sys.platform == "win32" and os.path.splitext(program)[1].lower() != '.exe':
249        program = program + '.exe'
250    pythonprogram = os.path.normpath(os.path.join(F2PYpath,program))
251    if is_exe(pythonprogram):
252        pythonpath = os.path.split(program)[0]
253        break
254else:
255    print ('python not found')
256    sys.exit()
257p = subprocess.Popen(pythonprogram, stdout=subprocess.PIPE, stdin=subprocess.PIPE, encoding='utf-8')
258p.stdin.write("""
259import sys,platform;
260print (str(sys.version_info[0]))
261print (str(sys.version_info[1]))
262print (platform.architecture()[0])
263import numpy as np
264print(np.__version__)
265sys.exit()""")
266p.stdin.close()
267p.wait()
268pyVersions = p.stdout.readlines()
269#pyVersions = platform.python_version_tuple()
270version = str(int(pyVersions[0])) + '.' + str(int(pyVersions[1]))
271PlatformBits = '64bits'
272if 'arm' in platform.machine() and sys.platform == "darwin":
273   PlatformBits = 'arm'
274elif 'arm' in platform.machine() and '32' in platform.architecture()[0]:
275   PlatformBits = 'arm32'
276elif 'aarch' in platform.machine():
277   PlatformBits = 'arm64'
278elif '32' in platform.architecture()[0]:
279   PlatformBits = '32bits'
280#PlatformBits = pyVersions[2][:-1]
281#
282# Set install location
283if ARGUMENTS.get('install', '').upper().startswith('T'):
284    InstallLoc = os.path.join('..','AllBinaries', GetBinaryDir())
285else:
286    InstallLoc = os.path.join('..','bin')
287#==========================================================================================
288# use the compiler choice to set compiler options, but don't change anything
289# specified on the command line
290if FCompiler == 'gfortran':
291    if FORTpath == "": FORTpath = GFORTpath
292    if sys.platform.startswith("linux") and "64" in PlatformBits:
293        if 'aarch' in platform.machine():
294            if FORTflags == "": FORTflags = ' -w -O2 -fPIC'
295            if F2PYflags == "": F2PYflags = '--fcompiler=gnu95 --f77exec=gfortran --f77flags="-fno-range-check"'
296        else:
297            if FORTflags == "": FORTflags = ' -w -O2 -fPIC -m64'
298            if F2PYflags == "": F2PYflags = '--fcompiler=gnu95 --f77exec=gfortran --f77flags="-fno-range-check -m64"' # --arch="-arch x86_64"'
299    elif sys.platform.startswith("linux") and 'arm' in platform.machine():
300        if FORTflags == "": FORTflags = ' -w -O2 -fPIC'
301        if F2PYflags == "": F2PYflags = '--fcompiler=gnu95 --f77exec=gfortran --f77flags="-fno-range-check"'
302    elif sys.platform.startswith("linux"):
303        if FORTflags == "": FORTflags = ' -w -O2 -fPIC -m32'
304        if F2PYflags == "": F2PYflags = '--fcompiler=gnu95 --f77exec=gfortran --f77flags="-fno-range-check -m32"'
305    elif sys.platform == "darwin": # now 64 bit only
306        if 'arm' in PlatformBits:
307            #LDFLAGS += " -arch x86_64 -m64"
308            if FORTflags == "": FORTflags = ' -w -O2'
309            if F2PYflags == "": F2PYflags = '--fcompiler=gnu95 --f77exec=gfortran --f77flags="-fno-range-check"'
310        else:
311            LDFLAGS += " -arch x86_64 -m64"
312            if FORTflags == "": FORTflags = ' -w -O2 -m64'
313            if F2PYflags == "": F2PYflags = '--fcompiler=gnu95 --f77exec=gfortran --f77flags="-fno-range-check -m64"'
314    elif sys.platform == "win32" and "64" in PlatformBits:
315        if FORTflags == "": FORTflags = ' -w -O2 -m64'
316        if F2PYflags == "":
317            F2PYflags = '--compiler=mingw32 --fcompiler=gfortran --f77flags="-fno-range-check -m64"'
318    elif sys.platform == "win32":
319        # the next line may need to be removed. When compiling with a 32-bit machine?
320        #if FORTflags == "": FORTflags = ' -w -O2 -m32'
321        if F2PYflags == "":
322            F2PYflags = '--compiler=mingw32 --fcompiler=gfortran --f77flags="-fno-range-check"'
323elif FCompiler == 'g77':
324    if FORTpath == "": FORTpath = G77path
325    if sys.platform == "win32":
326        if F2PYflags == "": F2PYflags = '--compiler=mingw32 --fcompiler=g77'
327        if FORTflags == "": FORTflags = ' -w -O2 -fno-automatic -finit-local-zero -malign-double -mwindows'
328    else:
329        if F2PYflags == "": F2PYflags = '--fcompiler=gnu --f77exec=g77 --f77flags="-fno-range-check"'
330
331else:
332    if FORTpath == "": print ('Likely error, FORTpath is not specified')
333    if F2PYflags == "":
334        print ('Error: specify a F2PYflags value')
335        sys.exit()
336if tmpdir:
337    F2PYflags += " --build-dir " + tmpdir
338#==========================================================================================
339# Setup build Environment
340if sys.platform == "win32":
341   env = Environment(ENV = os.environ)
342else:
343   env = Environment()
344# Define a builder to run f2py
345def generate_f2py(source, target, env, for_signature):
346    module = os.path.splitext(str(source[0]))[0]
347    if len(liblist) > 0:
348        for lib in liblist:
349            module = module + ' ' + str(lib)
350    f2pyprogram = os.path.normpath(os.path.join(F2PYpath,F2PYprog))
351    if os.path.splitext(F2PYprog)[1] == '.py':     # use f2py.py if no f2py[.exe]
352        f2pycmd = pythonprogram + ' ' + f2pyprogram + ' -c $SOURCE ' + ' -m ' + module + ' ' + F2PYflags
353    else:
354        f2pycmd = f2pyprogram + ' -c $SOURCE' + ' -m ' + module + ' ' + F2PYflags
355    if sys.platform == "win32":
356        installcmd = "copy " + os.path.splitext(str(source[0]))[0] + '*' + F2PYsuffix + ' ' + InstallLoc
357    else:
358        installcmd = "cp " + os.path.splitext(str(source[0]))[0] + '*' + F2PYsuffix + ' ' + InstallLoc
359    return [f2pycmd, installcmd]
360f2py = Builder(generator = generate_f2py)
361env.Append(BUILDERS = {'f2py' : f2py},)
362# create a builder for the fortran compiler for library compilation so we can control how it is done
363def generate_obj(source, target, env, for_signature):
364    dir = os.path.split(str(source[0]))[0]
365    obj = os.path.splitext(str(source[0]))[0]+'.o'
366    return os.path.join(FORTpath,FCompiler)  + ' -c $SOURCE ' + FORTflags + ' -I' + dir + ' -o' + obj
367fort = Builder(generator = generate_obj, suffix = '.o', src_suffix = '.for')
368# create a library builder so we can control how it is done on windows
369def generate_lib(source, target, env, for_signature):
370    srclst = ""
371    for s in source:
372      srclst += str(s) + " "
373    return os.path.join(FORTpath,'ar.exe')  + ' -rs $TARGET ' + srclst
374lib = Builder(generator = generate_lib, suffix = '.a',
375               src_suffix = '.o')
376env.Append(BUILDERS = {'fort' : fort, 'lib' : lib},)
377
378# Define a builder to compile and copy NIST*LATTICE programs
379def generate_nist(source, target, env, for_signature):
380    #exe = os.path.splitext(str(source[0]))[0] + EXEsuffix
381    exe = target
382#    cmd = FCompiler + ' $SOURCE ' + NISTlib[0] + ' -o ' + exe
383    cmd = FCompiler + ' $SOURCE ' + NISTlib[0] + ' -o $TARGET'
384    if sys.platform == "win32":
385        installcmd = "copy $TARGET " + InstallLoc
386    else:
387        installcmd = "mv $TARGET " + InstallLoc
388    return [cmd, installcmd]
389env.Append(BUILDERS = {'nist' : Builder(generator = generate_nist)},)
390
391
392#==========================================================================================
393# Setup build Environment
394#    add compiler, f2py & python to path
395if FORTpath != "":  env.PrependENVPath('PATH', FORTpath)
396if F2PYpath != "":  env.PrependENVPath('PATH', F2PYpath)
397if pythonpath != "" and pythonpath != F2PYpath: env.PrependENVPath('PATH', pythonpath)
398#   add other needed environment variables
399for var in ('LDFLAGS','SDKROOT'):
400    if eval(var) != "":
401       env['ENV'][var] = eval(var)
402       print("Setting environment variable {} to {}".format(var,eval(var)))
403if 'WINDIR' in os.environ: env['ENV']['WINDIR'] = os.environ['WINDIR']
404
405#==========================================================================================
406# finally ready to build something!
407# locate libraries to be built (subdirectories named *subs)
408liblist = []
409NISTlib = []
410for sub in glob.glob('*subs'):
411    filelist = []
412    for file in glob.glob(os.path.join(sub,'*.for'))+glob.glob(os.path.join(sub,'*.f')):
413        #target = os.path.splitext(file)[0]+'.o'
414        target = env.fort(file) # connect .o files to .for files
415        #print ('Compile: ',file, target)
416        filelist.append(target)
417    for file in glob.glob(os.path.join(sub,'*.f90')):
418        target = env.fort(file) # connect .o files to .f90 files
419        filelist.append(target)
420    #lib = Library(sub, Glob(os.path.join(sub,'*.for'))) # register library to be created
421    if sys.platform == "win32":
422       lib = env.lib(sub, filelist)
423    else:
424       lib = Library(sub, filelist) # register library to be created
425    if 'NIST' in lib[0].name:
426       NISTlib = [lib[0].name]
427    else:       
428       liblist.append(lib[0].name)
429    filename = str(lib[0])
430
431# find modules that need to be built
432modlist = []
433for src in glob.glob('*.for'):
434    #break # bail out early for testing
435    target = os.path.splitext(src)[0] + F2PYsuffix # xxx.pyd or xxx.so
436    out = env.f2py(target,src)
437    Clean(out, Glob(os.path.splitext(src)[0] + "*" + F2PYsuffix)) # this picks up old- & new-style .pyd/.so names
438    Depends(target, liblist) # make sure libraries are rebuilt if old
439    modlist.append(out[0].name)
440
441# NIST*LATTICE programs
442for src in 'LATTIC.f','convcell.f':
443    target = os.path.splitext(src)[0] + EXEsuffix
444    out = env.nist(target,src)
445    Clean(out, target)
446    Depends(target, NISTlib) # make sure library is rebuilt if old   
447#==========================================================================================
448# all done with setup, show the user the options and let scons do the work
449print (80*'=')
450for var in ['FCompiler','FORTpath','FORTflags','F2PYflags','F2PYpath','F2PYsuffix','LDFLAGS']:
451    print ('Variable',var,'is',eval(var))
452print ('Using python at', pythonprogram )
453print ('Python/f2py version =',version,PlatformBits)
454print ('Install directory is',InstallLoc)
455print ('Will build object libraries:',)
456for lib in liblist: print (" " + lib,)
457print ("")
458print ('f2py will build these modules:',)
459for mod in modlist: print (" " + mod,)
460print ("")
461print ('Use "scons help" to see build options')
462print (80*'=')
463#print (env.Dump())
464if 'help' in COMMAND_LINE_TARGETS: sys.exit()
465import datetime
466if not os.path.exists(InstallLoc) and not GetOption('no_exec'):
467    print('Creating '+InstallLoc)
468    os.makedirs(InstallLoc)
469if not GetOption('no_exec'):
470    fp = open(os.path.join(InstallLoc,'Build.notes.txt'),'w')
471    fp.write('Created {} on {}\n'.format(datetime.datetime.isoformat(datetime.datetime.now()),
472                                         platform.node()))
473    fp.close()
Note: See TracBrowser for help on using the repository browser.