source: trunk/fsource/SConstruct @ 5360

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

note use of requests package (now optional); deal w/wx4.2.0 bug; save NIST*LATTICE snapshot

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