diff options
-rw-r--r-- | .SRCINFO | 46 | ||||
-rw-r--r-- | 001-use_distribution_environment.patch | 154 | ||||
-rw-r--r-- | 002-fix_fsl_exec_empty_errorCode.patch | 12 | ||||
-rw-r--r-- | 003-fix_missing_LIB_PROB.patch | 35 | ||||
-rw-r--r-- | 004-fix_missing_LIB_PROB.patch | 35 | ||||
-rw-r--r-- | 004-fix_mist_discard.patch | 11 | ||||
-rw-r--r-- | 005-fix_cuda_thrust_include.patch | 76 | ||||
-rw-r--r-- | 006-compile_ptx2_without_std-c++11.patch | 10 | ||||
-rwxr-xr-x | PKGBUILD | 113 | ||||
-rw-r--r-- | buildSettings.mk | 139 | ||||
-rwxr-xr-x | externallibs.mk | 77 | ||||
-rw-r--r-- | fsl_exec.patch | 11 | ||||
-rw-r--r-- | fsl_sub | 544 | ||||
-rw-r--r-- | fslinstaller.py | 2892 | ||||
-rwxr-xr-x | imcp | 31 | ||||
-rwxr-xr-x | imglob | 31 | ||||
-rwxr-xr-x | immv | 31 | ||||
-rwxr-xr-x | systemvars.mk | 46 |
18 files changed, 3868 insertions, 426 deletions
@@ -1,51 +1,13 @@ pkgbase = fsl pkgdesc = A comprehensive library of analysis tools for FMRI, MRI and DTI brain imaging data - pkgver = 6.0.1 + pkgver = 6.0.7.7 pkgrel = 1 url = http://www.fmrib.ox.ac.uk/fsl/ arch = x86_64 license = custom - makedepends = boost - makedepends = fftw - depends = gd - depends = libxml2 - depends = libxml++2.6 - depends = gsl - depends = libpng - depends = nlopt - depends = newmat - depends = tcl - depends = tk - depends = zlib depends = python - depends = glu - depends = boost-libs - depends = vtk - depends = sqlite - depends = python3 - depends = fslpy - depends = bc - optdepends = cuda - source = http://www.fmrib.ox.ac.uk/fsldownloads/fsl-6.0.1-sources.tar.gz - source = http://www.fmrib.ox.ac.uk/fsldownloads/fsl-6.0.1-feeds.tar.gz - source = externallibs.mk - source = systemvars.mk - source = imcp - source = imglob - source = immv - source = 001-use_distribution_environment.patch - source = 002-fix_meldata_usage_of_ifstream.patch - source = 003-fix_fsl_exec_empty_errorCode.patch - sha256sums = ccab9709239340299b0ca034cb00d6ce0170b9e0d075b3adb55c556feacfb2da - sha256sums = 91aa756d5a052702cc68e41bcc9a64ba7c7f8853feb215d5a44eeb710c4a0fd0 - sha256sums = e3345af9d3a1bca157c3a5700c63c4d0e01da3cec525f8ffb8f1a04b048aeff1 - sha256sums = 326c73cf0fb07ef9436ec31dda00f1e77488949152aa908b50aa4059701b2984 - sha256sums = c61f185fbe7e297c4518e96377aa5ff4852f90eda0dbb9ae8edc5e24735e14ad - sha256sums = 7a1039cdc38b4d728f14efce3b0fda0cadc7bfcd3432556c3f3113985bf2720a - sha256sums = b6f61a6d5672b6684f19150f6e21ded1bd04ec6415dcf07a32291e4002bfa5d8 - sha256sums = b59921d9b76c07da6c775d63d5fe99ca5069a15827aa7a3d44c2e5eb6f3638d6 - sha256sums = 13d4cf35343e7a73bc2534c94b1b0d4db41c338d374e6982091e4cf7a421d420 - sha256sums = 64b4ccefa63a3cf920b185dd52e94b918c24f2cedaebcec8efb767bd80a6418a + options = !strip + source = fslinstaller.py + sha256sums = 3db63c9f53edc909b2264bd364c241d72945862726c305025694aae44e544b0c pkgname = fsl - diff --git a/001-use_distribution_environment.patch b/001-use_distribution_environment.patch index ef84b6e115d8..65bc781bfee2 100644 --- a/001-use_distribution_environment.patch +++ b/001-use_distribution_environment.patch @@ -37,38 +37,34 @@ export FSLTCLSH FSLWISH ---- a/fsl/extras/build 2016-11-15 15:30:21.000000000 +0100 -+++ b/fsl/extras/build 2018-11-25 14:10:48.382885194 +0100 -@@ -96,21 +96,20 @@ - BUILDICONV=1 +--- a/fsl/extras/build 2019-09-29 14:50:57.966635464 +0200 ++++ b/fsl/extras/build 2019-09-29 14:53:48.906589517 +0200 +@@ -96,21 +96,12 @@ + BUILDICONV=1 fi fi -PROJECTS="tcl tk" -+PROJECTS="" - if [ ${BUILDZLIB} -eq 1 ]; then +-if [ ${BUILDZLIB} -eq 1 ]; then - PROJECTS="${PROJECTS} zlib" -+ PROJECTS="${PROJECTS}" - fi +-fi -PROJECTS="${PROJECTS} libpng" -+PROJECTS="${PROJECTS}" - if [ ${BUILDICONV} -eq 1 ]; then +-if [ ${BUILDICONV} -eq 1 ]; then - PROJECTS="${PROJECTS} libiconv" -+ PROJECTS="${PROJECTS}" - fi +-fi -PROJECTS="${PROJECTS} libgd libgdc libprob libcprob newmat cprob newran fftw" --PROJECTS="${PROJECTS} boost libxml2-2.9.2 libxml++-2.34.0 libsqlite libnlopt ../include/armawrap/dummy_newmat" +-PROJECTS="${PROJECTS} boost libxml2-2.9.2 libxmlpp libsqlite libnlopt ../include/armawrap/dummy_newmat" +PROJECTS="${PROJECTS} libgdc libprob libcprob cprob newran ../include/armawrap/dummy_newmat" for projname in $PROJECTS; do if [ -d $FSLESRCDIR/$projname ] ; then - buildIt $FSLESRCDIR $projname 1 + buildIt $FSLESRCDIR $projname 1 elif [ "$projname" = "boost" -a -d ${FSLEXTINC}/boost/boost ]; then - buildIt $FSLEXTINC boost 0 + buildIt $FSLEXTINC 0 fi done ---- a/fsl/src/mist-clean/Makefile 2018-10-17 13:00:28.000000000 +0200 -+++ b/fsl/src/mist-clean/Makefile 2018-11-25 11:26:42.643047048 +0100 +--- a/fsl/src/mist/Makefile 2018-10-17 13:00:28.000000000 +0200 ++++ b/fsl/src/mist/Makefile 2018-11-25 11:26:42.643047048 +0100 @@ -2,15 +2,15 @@ NLOPT_INC = ${FSLEXTINC} @@ -104,129 +100,3 @@ + ${LIBRT} HDRS = common/gibbsshapemodel.h common/mrfshapemodel.h common/mvnshapemodel.h common/plotting.h common/profilefilters.h common/profilemixtures.h common/profilemodel.h common/profilepriors.h common/serialisation.h common/shape.h common/shapemodel.h common/stats.h common/transformation.h mist/builddate.h - ---- a/fsl/config/_FSLMACHTYPE/externallibs.mk 2018-10-10 13:12:48.000000000 +0200 -+++ b/fsl/config/_FSLMACHTYPE/externallibs.mk 2018-11-25 11:26:48.363038307 +0100 -@@ -7,26 +7,27 @@ - FSLEXTBIN=${FSLDIR}/extras/bin - - # GD library --LIB_GD = ${FSLEXTLIB} --INC_GD = ${FSLEXTINC} -+LIB_GD = /usr/lib -+INC_GD = /usr/include - - # GDC library - LIB_GDC = ${FSLEXTLIB} - INC_GDC = ${FSLEXTINC}/libgdc - - # LIBXML2 library --INC_XML2 = ${FSLEXTINC}/libxml2 -+INC_XML2 = /usr/include/libxml2 - - # LIBXML++ library --INC_XML++ = ${FSLEXTINC}/libxml++-2.6 --INC_XML++CONF = ${FSLEXTLIB}/libxml++-2.6/include -+INC_XML++ = /usr/include/libxml++-2.6 -+INC_XML++CONF = /usr/lib/libxml++-2.6/include -+ - # GSL library --LIB_GSL = ${FSLEXTLIB} --INC_GSL = ${FSLEXTINC}/gsl -+LIB_GSL = /usr/lib -+INC_GSL = /usr/include/gsl - - # PNG library --LIB_PNG = ${FSLEXTLIB} --INC_PNG = ${FSLEXTINC} -+LIB_PNG = /usr/lib -+INC_PNG = /usr/include/libpng1.6 - - # PROB library - LIB_PROB = ${FSLEXTLIB} -@@ -38,7 +39,9 @@ - - # NEWMAT library - #LIB_NEWMAT = ${FSLEXTLIB} -llapack -lblas or just -lopenblas --LIB_NEWMAT = ${FSLEXTLIB} -lopenblas -+#LIB_NEWMAT = /usr/lib/newmat -+#INC_NEWMAT = /usr/include/newmat -+LIB_NEWMAT = ${FSLEXTLIB} -llapack -lblas - INC_NEWMAT = ${FSLEXTINC}/armawrap/armawrap -DARMA_USE_LAPACK -DARMA_USE_BLAS -DARMA_64BIT_WORD - - # NEWRAN library -@@ -46,29 +49,29 @@ - INC_NEWRAN = ${FSLEXTINC}/newran - - # ZLIB library --LIB_ZLIB = /lib64 -+LIB_ZLIB = /usr/lib - INC_ZLIB = /usr/include - - # BOOST library --BOOSTDIR = ${FSLEXTINC}/boost --LIB_BOOST = ${BOOSTDIR} --INC_BOOST = ${BOOSTDIR} -+#BOOSTDIR = ${FSLEXTINC}/boost -+LIB_BOOST = /usr/lib -+INC_BOOST = /usr/include/boost - - # QT library --QTDIR = /usr/lib/qt3 --LIB_QT = ${QTDIR}/lib --INC_QT = ${QTDIR}/include -+#QTDIR = /usr/lib/qt5 -+LIB_QT = /usr/lib -+INC_QT = ${QTDIR}/include/qt - - # QWT library --QWTDIR = /usr/local/qwt --LIB_QWT = ${QWTDIR}/lib --INC_QWT = ${QWTDIR}/include -+#QWTDIR = /usr/local/qwt -+LIB_QWT = /usr/lib -+INC_QWT = /usr/include/qwt - - # FFTW3 library --LIB_FFTW3 = ${FSLEXTLIB} --INC_FFTW3 = ${FSLEXTINC}/fftw3 -+LIB_FFTW3 = /usr/lib -+INC_FFTW3 = /usr/include - - # VTK library --VTKDIR_INC = /home/fs0/cowboy/var/caper_linux_64-gcc4.4/VTK7/include/vtk-7.0 --VTKDIR_LIB = /home/fs0/cowboy/var/caper_linux_64-gcc4.4/VTK7/lib --VTKSUFFIX = -7.0 -\ No newline at end of file -+VTKDIR_INC = /usr/include/vtk -+VTKDIR_LIB = /usr/lib -+VTKSUFFIX = ---- a/fsl/config/_FSLMACHTYPE/systemvars.mk 2017-09-04 14:09:26.000000000 +0200 -+++ b/fsl/config/_FSLMACHTYPE/systemvars.mk 2018-11-25 11:26:48.363038307 +0100 -@@ -8,7 +8,7 @@ - CP = /bin/cp - MV = /bin/mv - INSTALL = install -p --TCLSH = ${FSLDIR}/bin/fsltclsh -+TCLSH = /usr/bin/tclsh - RANLIB = echo - - FSLML = ${FSLDIR}/bin/fslml -@@ -30,14 +30,14 @@ - - DEPENDFLAGS = -MM - --OPTFLAGS = -g -O3 -fexpensive-optimizations ${ARCHFLAGS} -+OPTFLAGS = -march=native -g -O3 -fexpensive-optimizations ${ARCHFLAGS} - MACHDBGFLAGS = -g --GNU_ANSI_FLAGS = -Wall -ansi -pedantic -Wno-long-long -+GNU_ANSI_FLAGS = -Wall -ansi -pedantic -std=c++11 -Wno-long-long - SGI_ANSI_FLAGS = -ansi -fullwarn - ANSI_FLAGS = ${GNU_ANSI_FLAGS} - - # CUDA development environment --CUDA_INSTALLATION = /opt/cuda-7.5 -+CUDA_INSTALLATION = /opt/cuda - GENCODE_FLAGS = $(shell ${FSLDIR}/config/common/supportedGencodes.sh ${CUDA_INSTALLATION}) - LIB_CUDA = ${CUDA_INSTALLATION}/lib64 - INC_CUDA = ${CUDA_INSTALLATION}/include diff --git a/002-fix_fsl_exec_empty_errorCode.patch b/002-fix_fsl_exec_empty_errorCode.patch new file mode 100644 index 000000000000..5e8cb0269cd2 --- /dev/null +++ b/002-fix_fsl_exec_empty_errorCode.patch @@ -0,0 +1,12 @@ +--- a/fsl/src/misc_tcl/fsl_exec.tcl 2018-10-23 18:49:44.000000000 +0200 ++++ b/fsl/src/misc_tcl/fsl_exec.tcl 2018-11-25 18:52:30.636934316 +0100 +@@ -175,6 +175,9 @@ + set logout "" + } + ++ # make sure errorCode is set ++ if { ! [ info exists ::errorCode ] } { set ::errorCode "NONE" } ++ + # run and log the actual command + if { $do_logout } { + fsl:echo $logout "\n$thecommand" diff --git a/003-fix_missing_LIB_PROB.patch b/003-fix_missing_LIB_PROB.patch new file mode 100644 index 000000000000..abd3dbb4b631 --- /dev/null +++ b/003-fix_missing_LIB_PROB.patch @@ -0,0 +1,35 @@ +--- a/fsl/src/eddy/Makefile 2019-09-29 21:41:53.118138982 +0200 ++++ b/fsl/src/eddy/Makefile 2019-09-29 21:25:19.118338652 +0200 +@@ -62,7 +62,7 @@ + endif + EXENAME=eddy${TMPNAME_1}${TMPNAME_2} + endif +-USRLDFLAGS=-L${LIB_NEWMAT} -Wl,-rpath,/opt/fmrib/fsl/lib -rdynamic ++USRLDFLAGS=-L${LIB_NEWMAT} -L${LIB_PROB} -Wl,-rpath,/opt/fmrib/fsl/lib -rdynamic + USRINCFLAGS=-I. -I${INC_NEWMAT} -I${INC_PROB} -I${INC_BOOST} -I${INC_CUDA} -I$(CBF_DIR) -I$(INC_BASISFIELD) + + OBJS=eddy.o BiasFieldEstimatorImpl.o MoveBySuscCF.o PostEddyAlignShellsFunctions.o EddyCommandLineOptions.o ECModels.o KMatrix.o HyParEstimator.o ECScanClasses.o EddyUtils.o EddyHelperClasses.o DiffusionGP.o b0Predictor.o + +--- a/fsl/src/asl_mfree/Makefile 2019-09-29 22:52:54.221367251 +0200 ++++ b/fsl/src/asl_mfree/Makefile 2019-09-29 22:53:23.577360889 +0200 +@@ -3,7 +3,7 @@ + PROJNAME = asl_mfree + + USRINCFLAGS = -I${INC_NEWMAT} -I${INC_ZLIB} +-USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_ZLIB} ++USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_ZLIB} -L${LIB_PROB} + + FSLVERSION= $(shell cat ${FSLDIR}/etc/fslversion | head -c 1) + ifeq ($(FSLVERSION), 5) + +--- a/fsl/src/oxford_asl/Makefile 2019-09-29 22:52:27.621373018 +0200 ++++ b/fsl/src/oxford_asl/Makefile 2019-09-29 22:53:41.237357074 +0200 +@@ -3,7 +3,7 @@ + PROJNAME = oxford_asl + + USRINCFLAGS = -I${INC_NEWMAT} -I${INC_ZLIB} +-USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_ZLIB} ++USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_ZLIB} -L${LIB_PROB} + + FSLVERSION= $(shell cat ${FSLDIR}/etc/fslversion | head -c 1) + ifeq ($(FSLVERSION), 5) diff --git a/004-fix_missing_LIB_PROB.patch b/004-fix_missing_LIB_PROB.patch new file mode 100644 index 000000000000..abd3dbb4b631 --- /dev/null +++ b/004-fix_missing_LIB_PROB.patch @@ -0,0 +1,35 @@ +--- a/fsl/src/eddy/Makefile 2019-09-29 21:41:53.118138982 +0200 ++++ b/fsl/src/eddy/Makefile 2019-09-29 21:25:19.118338652 +0200 +@@ -62,7 +62,7 @@ + endif + EXENAME=eddy${TMPNAME_1}${TMPNAME_2} + endif +-USRLDFLAGS=-L${LIB_NEWMAT} -Wl,-rpath,/opt/fmrib/fsl/lib -rdynamic ++USRLDFLAGS=-L${LIB_NEWMAT} -L${LIB_PROB} -Wl,-rpath,/opt/fmrib/fsl/lib -rdynamic + USRINCFLAGS=-I. -I${INC_NEWMAT} -I${INC_PROB} -I${INC_BOOST} -I${INC_CUDA} -I$(CBF_DIR) -I$(INC_BASISFIELD) + + OBJS=eddy.o BiasFieldEstimatorImpl.o MoveBySuscCF.o PostEddyAlignShellsFunctions.o EddyCommandLineOptions.o ECModels.o KMatrix.o HyParEstimator.o ECScanClasses.o EddyUtils.o EddyHelperClasses.o DiffusionGP.o b0Predictor.o + +--- a/fsl/src/asl_mfree/Makefile 2019-09-29 22:52:54.221367251 +0200 ++++ b/fsl/src/asl_mfree/Makefile 2019-09-29 22:53:23.577360889 +0200 +@@ -3,7 +3,7 @@ + PROJNAME = asl_mfree + + USRINCFLAGS = -I${INC_NEWMAT} -I${INC_ZLIB} +-USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_ZLIB} ++USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_ZLIB} -L${LIB_PROB} + + FSLVERSION= $(shell cat ${FSLDIR}/etc/fslversion | head -c 1) + ifeq ($(FSLVERSION), 5) + +--- a/fsl/src/oxford_asl/Makefile 2019-09-29 22:52:27.621373018 +0200 ++++ b/fsl/src/oxford_asl/Makefile 2019-09-29 22:53:41.237357074 +0200 +@@ -3,7 +3,7 @@ + PROJNAME = oxford_asl + + USRINCFLAGS = -I${INC_NEWMAT} -I${INC_ZLIB} +-USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_ZLIB} ++USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_ZLIB} -L${LIB_PROB} + + FSLVERSION= $(shell cat ${FSLDIR}/etc/fslversion | head -c 1) + ifeq ($(FSLVERSION), 5) diff --git a/004-fix_mist_discard.patch b/004-fix_mist_discard.patch new file mode 100644 index 000000000000..9ed361fb3c2a --- /dev/null +++ b/004-fix_mist_discard.patch @@ -0,0 +1,11 @@ +--- a/fsl/src/mist/common/shape.cpp 2021-07-15 10:44:27.000000000 +0000 ++++ b/fsl/src/mist/common/shape.cpp 2021-11-24 14:12:38.684538565 +0000 +@@ -354,7 +354,7 @@ + for (vtkIdType i = 0; i < cells->GetNumberOfIds(); i++) + { + vtkIdType npoints; +- vtkIdType *points; ++ const vtkIdType *points; + + m_polyData->GetCellPoints(cells->GetId(i), npoints, points); + diff --git a/005-fix_cuda_thrust_include.patch b/005-fix_cuda_thrust_include.patch new file mode 100644 index 000000000000..1870755c2f0c --- /dev/null +++ b/005-fix_cuda_thrust_include.patch @@ -0,0 +1,76 @@ +# Author: Caspar van Leeuwen (SURF) +# FSL includes thrust headers by doing #include <device_vector.h> +# and then include -I$CUDA_ROOT/include/thrust +# This results in errors like +# /sw/arch/Debian10/EB_production/2019/software/CUDA/10.1.243/include/cuda_runtime_api.h(7397): +# error: identifier "UINT_MAX" is undefined +# This is normally defined in the standard C header 'limits.h'. +# However, by doing -I$CUDA_ROOT/include/thrust, the limits.h from Thrust is picked up instead! +# According to the Thrust docs, includes should be done as #include <thrust/device_vector.h> +# This way, -I$CUDA_ROOT/include is sufficient, and the limits.h from thrust +# no longer erroneously gets picked up. +diff -Nru a/fsl/src/fdt/CUDA/diffmodels.cuh fsl/src/fdt/CUDA/diffmodels.cuh +--- a/fsl/src/fdt/CUDA/diffmodels.cuh 2020-06-09 20:41:17.117128671 +0200 ++++ b/fsl/src/fdt/CUDA/diffmodels.cuh 2020-06-09 20:42:44.045211731 +0200 +@@ -66,7 +66,7 @@ + University, to negotiate a licence. Contact details are: + <a href="/cgi-bin/wa-jisc.exe?LOGON=A3%3Dind2006%26L%3DFSL%26E%3Dbase64%26P%3D3480627%26B%3D------%253D_Part_434001_1485233425.1591892113317%26T%3Dtext%252Fx-patch%3B%2520name%3D%2522FSL-6.0.2_thrust.patch%2522%26N%3DFSL-6.0.2_thrust.patch%26attachment%3Dq%26XSS%3D3" target="_parent" >[log in to unmask]</a> quoting Reference Project 9564, FSL.*/ + +-#include <device_vector.h> ++#include <thrust/device_vector.h> + + void fit_PVM_single( //INPUT + const vector<ColumnVector> datam_vec, +diff -Nru a/fsl/src/fdt/CUDA/runmcmc.cu fsl/src/fdt/CUDA/runmcmc.cu +--- a/fsl/src/fdt/CUDA/runmcmc.cu 2020-06-09 20:41:17.101128655 +0200 ++++ b/fsl/src/fdt/CUDA/runmcmc.cu 2020-06-09 20:43:13.649240030 +0200 +@@ -71,8 +71,8 @@ + #include "runmcmc_kernels.cu" + #include "sync_check.h" + +-#include <host_vector.h> +-#include <device_vector.h> ++#include <thrust/host_vector.h> ++#include <thrust/device_vector.h> + + #include <time.h> + #include <sys/time.h> +diff -Nru a/fsl/src/fdt/CUDA/xfibres_gpu.cu fsl/src/fdt/CUDA/xfibres_gpu.cu +--- a/fsl/src/fdt/CUDA/xfibres_gpu.cu 2020-06-09 20:41:17.117128671 +0200 ++++ b/fsl/src/fdt/CUDA/xfibres_gpu.cu 2020-06-09 20:44:00.621284957 +0200 +@@ -76,8 +76,8 @@ + #include "samples.h" + #include "options.h" + +-#include <host_vector.h> +-#include <device_vector.h> ++#include <thrust/host_vector.h> ++#include <thrust/device_vector.h> + + #include <time.h> + #include <sys/time.h> +diff -Nru a/fsl/src/fdt/CUDA/xfibres_gpu.cuh fsl/src/fdt/CUDA/xfibres_gpu.cuh +--- a/fsl/src/fdt/CUDA/xfibres_gpu.cuh 2020-06-09 20:41:17.113128667 +0200 ++++ b/fsl/src/fdt/CUDA/xfibres_gpu.cuh 2020-06-09 20:43:38.469263769 +0200 +@@ -67,8 +67,8 @@ + <a href="/cgi-bin/wa-jisc.exe?LOGON=A3%3Dind2006%26L%3DFSL%26E%3Dbase64%26P%3D3480627%26B%3D------%253D_Part_434001_1485233425.1591892113317%26T%3Dtext%252Fx-patch%3B%2520name%3D%2522FSL-6.0.2_thrust.patch%2522%26N%3DFSL-6.0.2_thrust.patch%26attachment%3Dq%26XSS%3D3" target="_parent" >[log in to unmask]</a> quoting Reference Project 9564, FSL.*/ + + #include "newimage/newimageall.h" +-#include <host_vector.h> +-#include <device_vector.h> ++#include <thrust/host_vector.h> ++#include <thrust/device_vector.h> + + #include "fibre_gpu.h" + #include <curand_kernel.h> +--- a/fsl/src/fdt/Makefile 2020-08-25 21:32:15.316740340 +0200 ++++ b/fsl/src/fdt/Makefile 2020-08-25 21:34:28.163670755 +0200 +@@ -171,7 +171,7 @@ + ${CXX} ${CXXFLAGS} ${LDFLAGS} -o $@ ${EDDYCOMBINEOBJS} ${DLIBS} + + ${LIBBEDPOSTX_CUDA}: +- ${NVCC} --shared --compiler-options '-fPIC' -o CUDA/libbedpostx_cuda.so CUDA/init_gpu.cu CUDA/samples.cu CUDA/diffmodels.cu CUDA/runmcmc.cu CUDA/xfibres_gpu.cu -O3 ${GENCODE_FLAGS} -lcudart -lcuda -lcurand -I. -L${LIB_CUDA} -L${LIB_CUDA}/stubs -ICUDA/options -I${INC_NEWMAT} -I${FSLDIR}/include -I${INC_BOOST} -I${INC_CUDA} -I${INC_CUDA}/thrust -maxrregcount=64 ++ ${NVCC} --shared --compiler-options '-fPIC' -o CUDA/libbedpostx_cuda.so CUDA/init_gpu.cu CUDA/samples.cu CUDA/diffmodels.cu CUDA/runmcmc.cu CUDA/xfibres_gpu.cu -O3 ${GENCODE_FLAGS} -lcudart -lcuda -lcurand -I. -L${LIB_CUDA} -L${LIB_CUDA}/stubs -ICUDA/options -I${INC_NEWMAT} -I${FSLDIR}/include -I${INC_BOOST} -I${INC_CUDA} -I${INC_CUDA} -maxrregcount=64 + @if [ ! -d ${FSLDEVDIR}/lib/ ] ; then ${MKDIR} ${FSLDEVDIR}/lib ; fi + ${CP} -rf CUDA/libbedpostx_cuda.so ${FSLDEVDIR}/lib + diff --git a/006-compile_ptx2_without_std-c++11.patch b/006-compile_ptx2_without_std-c++11.patch new file mode 100644 index 000000000000..2067c5d4df5e --- /dev/null +++ b/006-compile_ptx2_without_std-c++11.patch @@ -0,0 +1,10 @@ +--- a/fsl/src/ptx2/Makefile 2020-01-24 11:47:42.000000000 +0100 ++++ b/fsl/src/ptx2/Makefile 2020-08-25 23:34:22.215595885 +0200 +@@ -24,6 +24,7 @@ + USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_NEWRAN} -L${LIB_CPROB} -L${LIB_PROB} -L${LIB_ZLIB} + + DLIBS = -lnewmeshclass -lwarpfns -lbasisfield -lfslsurface -lfslvtkio -lmeshclass -lnewimage -lutils -lmiscmaths -lnewmat -lnewran -lNewNifti -lgiftiio -lexpat -lfirst_lib -lznz -lcprob -lutils -lprob -lm -lz ++GNU_ANSI_FLAGS = -Wall -ansi -pedantic -Wno-long-long + + CCOPS=ccops + PTX=probtrackx2 @@ -2,114 +2,35 @@ # Contributor: fishburn <frankthefishburn@gmail.com> pkgname=fsl -pkgver=6.0.1 +pkgver=6.0.7.7 pkgrel=1 pkgdesc="A comprehensive library of analysis tools for FMRI, MRI and DTI brain imaging data" arch=("x86_64") url="http://www.fmrib.ox.ac.uk/fsl/" -license=(custom) -depends=(gd libxml2 libxml++2.6 gsl libpng nlopt newmat tcl tk zlib python glu boost-libs vtk sqlite python3 fslpy bc) -makedepends=(boost fftw) -optdepends=(cuda) -source=("http://www.fmrib.ox.ac.uk/fsldownloads/fsl-${pkgver}-sources.tar.gz" - "http://www.fmrib.ox.ac.uk/fsldownloads/fsl-${pkgver}-feeds.tar.gz" - "externallibs.mk" - "systemvars.mk" - "imcp" - "imglob" - "immv" - "001-use_distribution_environment.patch" - "002-fix_meldata_usage_of_ifstream.patch" - "003-fix_fsl_exec_empty_errorCode.patch") +license=('custom') +depends=('python') +source=("fslinstaller.py") +options=('!strip') # Added as it took hours to do this without substantial benefit -sha256sums=('ccab9709239340299b0ca034cb00d6ce0170b9e0d075b3adb55c556feacfb2da' - '91aa756d5a052702cc68e41bcc9a64ba7c7f8853feb215d5a44eeb710c4a0fd0' - 'e3345af9d3a1bca157c3a5700c63c4d0e01da3cec525f8ffb8f1a04b048aeff1' - '326c73cf0fb07ef9436ec31dda00f1e77488949152aa908b50aa4059701b2984' - 'c61f185fbe7e297c4518e96377aa5ff4852f90eda0dbb9ae8edc5e24735e14ad' - '7a1039cdc38b4d728f14efce3b0fda0cadc7bfcd3432556c3f3113985bf2720a' - 'b6f61a6d5672b6684f19150f6e21ded1bd04ec6415dcf07a32291e4002bfa5d8' - 'b59921d9b76c07da6c775d63d5fe99ca5069a15827aa7a3d44c2e5eb6f3638d6' - '13d4cf35343e7a73bc2534c94b1b0d4db41c338d374e6982091e4cf7a421d420' - '64b4ccefa63a3cf920b185dd52e94b918c24f2cedaebcec8efb767bd80a6418a') - -prepare() { - cd "${srcdir}" - export FSLDIR="${srcdir}/fsl" - . "${FSLDIR}/etc/fslconf/fsl.sh" - export FSLMACHTYPE=$(${FSLDIR}/etc/fslconf/fslmachtype.sh) - mkdir "${FSLDIR}/config/${FSLMACHTYPE}" - # Use config linux_64-gcc4.8 as template - cp "${srcdir}"/{externallibs.mk,systemvars.mk} "${FSLDIR}/config/${FSLMACHTYPE}" - - # Apply patches - patch -Np1 -i "${srcdir}/001-use_distribution_environment.patch" - patch -Np1 -i "${srcdir}/002-fix_meldata_usage_of_ifstream.patch" - # From https://www.jiscmail.ac.uk/cgi-bin/webadmin?A2=fsl;e8fa48c1.1501 - patch -Np1 -i "${srcdir}/003-fix_fsl_exec_empty_errorCode.patch" - - # Insert makepkg build flags into configuration - sed -i '0,/${AccumulatedIncFlags}/{s^${AccumulatedIncFlags}^& '"${CFLAGS}"'^}' "${srcdir}/fsl/config/common/vars.mk" - sed -i '0,/${AccumulatedIncFlags}/{s^${AccumulatedIncFlags}^& '"${CPPFLAGS}"'^}' "${srcdir}/fsl/config/common/vars.mk" - sed -i '1,/${AccumulatedIncFlags}/!{s^${AccumulatedIncFlags}^& '"${CXXFLAGS}"'^}' "${srcdir}/fsl/config/common/vars.mk" - sed -i '1,/${AccumulatedIncFlags}/!{s^${AccumulatedIncFlags}^& '"${CPPFLAGS}"'^}' "${srcdir}/fsl/config/common/vars.mk" - sed -i 's^LDFLAGS = .*$^& '"${LDFLAGS}"'^g' "${srcdir}/fsl/config/common/vars.mk" -} +sha256sums=('3db63c9f53edc909b2264bd364c241d72945862726c305025694aae44e544b0c') build() { - export FSLDIR="${srcdir}/fsl" - cd "${FSLDIR}" - ./build - - # Install missing binaries, which are no longer shipped (depends on fslpy) - /usr/bin/install -m 755 "${srcdir}"/{imcp,imglob,immv} "${srcdir}/fsl/bin" -} - -check() { - export FSLDIR="${srcdir}/fsl" - export FEEDSDIR="${srcdir}/feeds" - . "${FSLDIR}/etc/fslconf/fsl.sh" - cd "${FEEDSDIR}" - time ./RUN all + export TMPFSLDIR="${srcdir}/fsl" + mkdir -p "${TMPFSLDIR}" + /usr/bin/python3 "${srcdir}"/fslinstaller.py -n -o -d "${TMPFSLDIR}" -V "${pkgver}" } package() { - rm -rf "${srcdir}/fsl/src" - rm -rf "${srcdir}/fsl/extras/src" - rm -rf "${srcdir}/fsl/extras/include" - mkdir -p "${pkgdir}/opt/fsl" - - cp -r "${srcdir}/fsl/bin" "${pkgdir}/opt/fsl/" - cp -r "${srcdir}/fsl/data" "${pkgdir}/opt/fsl/" - cp -r "${srcdir}/fsl/doc" "${pkgdir}/opt/fsl/" - cp -r "${srcdir}/fsl/etc" "${pkgdir}/opt/fsl/" - cp -r "${srcdir}/fsl/extras" "${pkgdir}/opt/fsl/" - cp -r "${srcdir}/fsl/lib" "${pkgdir}/opt/fsl/" - cp -r "${srcdir}/fsl/refdoc" "${pkgdir}/opt/fsl/" - cp -r "${srcdir}/fsl/tcl" "${pkgdir}/opt/fsl/" - cp -r "${srcdir}/feeds" "${pkgdir}/opt/fsl/" - + # Replace paths and move files in place + find "${TMPFSLDIR}" -type f -exec grep -Iq . {} \; -exec sed -i "s|${srcdir}|/opt|g" {} + + mkdir "${pkgdir}"/opt + mv "${TMPFSLDIR}" "${pkgdir}"/opt + # Copy license + mkdir -p "${pkgdir}"/usr/share/licenses/fsl + cp "${pkgdir}"/opt/fsl/LICENCE.FSL "${pkgdir}"/usr/share/licenses/fsl/LICENSE + # Setup shell environment mkdir -p "${pkgdir}/etc/profile.d" echo 'FSLDIR=/opt/fsl' > "${pkgdir}/etc/profile.d/fsl.sh" echo '. ${FSLDIR}/etc/fslconf/fsl.sh' >> "${pkgdir}/etc/profile.d/fsl.sh" echo 'export FSLDIR' >> "${pkgdir}/etc/profile.d/fsl.sh" - echo 'export PATH=$PATH:${FSLDIR}/bin' >> "${pkgdir}/etc/profile.d/fsl.sh" - - mkdir -p "${pkgdir}/usr/share/licenses/fsl" - grep -v \< "${srcdir}/fsl/doc/fsl/licence.html" | cat -s > "${pkgdir}/usr/share/licenses/fsl/LICENSE" - - # Fix permissions - find "${pkgdir}" -type f -exec chmod 644 {} \; - find "${pkgdir}" -type d -exec chmod 755 {} \; - find "${pkgdir}/opt/fsl/bin" -exec chmod 755 {} \; - find "${pkgdir}/opt/fsl/etc/fslconf" -exec chmod 755 {} \; - chmod 755 "${pkgdir}/etc/profile.d/fsl.sh" - - mkdir -p "${pkgdir}/opt/fsl/feeds/results" - chmod -R 777 "${pkgdir}/opt/fsl/feeds/results" - chmod 755 "${pkgdir}/opt/fsl/feeds/RUN" - - # Clean up - find "${pkgdir}" -empty -delete - find "${pkgdir}" -type f -exec sed -i 's^/usr/local/fsl^/opt/fsl^g' "{}" \; } diff --git a/buildSettings.mk b/buildSettings.mk new file mode 100644 index 000000000000..4521b1063169 --- /dev/null +++ b/buildSettings.mk @@ -0,0 +1,139 @@ +# This master make file is based on the "old" +# systemvars.mk and externallibs.mk files from: +# apple-darwin13-llvm6.0 +# linux_64-gcc4.4 +# linux_64-gcc4.8 + +# get system type (Darwin, Linux) +SYSTYPE := $(shell uname -s) +##################################################################### +# +# System Vars (common) +# +##################################################################### +SHELL = /bin/sh +RM = /bin/rm +CP = /bin/cp +MV = /bin/mv +CHMOD = /bin/chmod +MKDIR = /bin/mkdir +INSTALL = install -p +TCLSH = /usr/bin/tclsh +DEPENDFLAGS = -MM +MACHDBGFLAGS = -g +##################################################################### +# +# External libs (common) +# +##################################################################### +FSLEXTLIB=${FSLDIR}/extras/lib +FSLEXTINC=${FSLDIR}/extras/include +FSLEXTBIN=${FSLDIR}/extras/bin +# CEPHES library +LIB_CEPHES = ${FSLEXTLIB} +INC_CEPHES = ${FSLEXTINC}/cephes +# GD library +LIB_GD = /usr/lib +INC_GD = /usr/include +# GDC library +LIB_GDC = ${FSLEXTLIB} +INC_GDC = ${FSLEXTINC}/libgdc +# GSL library +LIB_GSL = /usr/lib +INC_GSL = /usr/include/gsl +# PNG library +LIB_PNG = /usr/lib +INC_PNG = /usr/include/libpng16 +# PROB library; added -I${FSLEXTINC} for 6.0.5 +LIB_PROB = ${FSLEXTLIB} +INC_PROB = ${FSLEXTINC}/cprob -I${FSLEXTINC} +# CPROB library +LIB_CPROB = ${FSLEXTLIB} +INC_CPROB = ${FSLEXTINC}/ +# NEWRAN library +LIB_NEWRAN = ${FSLEXTLIB} +INC_NEWRAN = ${FSLEXTINC}/newran +# BOOST library +BOOSTDIR = ${FSLEXTINC}/boost +LIB_BOOST = /usr/lib +INC_BOOST = /usr/include/boost +# QWT library +QWTDIR = /usr +INC_QWT = ${QWTDIR}/include/qwt +LIB_QWT = ${QWTDIR}/lib +# FFTW3 library +LIB_FFTW3 = /usr/lib +INC_FFTW3 = /usr/include +# LIBXML2 library +INC_XML2 = /usr/include/libxml2 +# LIBXML++ library +INC_XML++ = /usr/include/libxml++-2.6 +INC_XML++CONF = /usr/lib/libxml++-2.6/include +# NEWMAT library/armadillo +INC_NEWMAT = ${FSLEXTINC}/armawrap/armawrap -DARMA_USE_LAPACK -DARMA_USE_BLAS -DARMA_64BIT_WORD +##################################################################### +# +# Linux specific sys vars and ext libs +# Makefile auto-detects gcc version for Linux +# +##################################################################### +ifeq ($(SYSTYPE), Linux) +############### System Vars ##################################### +CC = gcc +CXX = g++ +CXX11 = g++ +CSTATICFLAGS = -static +CXXSTATICFLAGS = -static +ARCHFLAGS = -m64 +ARCHLDFLAGS = -Wl,-rpath,'$$ORIGIN/../lib' +PARALLELFLAGS = -fopenmp +OPTFLAGS = -g -O3 -fexpensive-optimizations ${ARCHFLAGS} +GNU_ANSI_FLAGS = -Wall -ansi -pedantic -Wno-long-long -std=c++11 +SGI_ANSI_FLAGS = -ansi -fullwarn +ANSI_FLAGS = ${GNU_ANSI_FLAGS} +RANLIB = echo +FSLML = ${FSLDIR}/bin/fslml +# CUDA development environment +CUDAVER := $(or $(CUDAVER),9.1) +#$(info $$CUDAVER is [${CUDAVER}]) +CUDA_INSTALLATION = /opt/cuda +GENCODE_FLAGS = $(shell ${FSLDIR}/config/common/supportedGencodes.sh ${CUDA_INSTALLATION}) +LIB_CUDA = ${CUDA_INSTALLATION}/lib64 +INC_CUDA = ${CUDA_INSTALLATION}/include +NVCC = ${CUDA_INSTALLATION}/bin/nvcc +############### External Libs ##################################### +# ZLIB library +LIB_ZLIB = /usr/lib +INC_ZLIB = /usr/include +# QT library +QTDIR = /usr/ +LIB_QT = ${QTDIR}/lib +INC_QT = ${QTDIR}/include/qt +# VTK library +VTKDIR_INC = /usr/include/vtk +VTKDIR_LIB = /usr/lib +VTKSUFFIX = +# openblas +LIB_NEWMAT = /usr/lib -llapack -lopenblas +# get and then parse gcc version to run context specific builds +#GCCVER := $(shell gcc -dumpversion) +#GCCARR = $(subst ., ,$(GCCVER)) +#$(info $(GCCVER)) +#GCCMAJ = $(word 1, $(GCCARR)) +#GCCMIN = $(word 2, $(GCCARR)) +#GCCPAT = $(word 3, $(GCCARR)) +#$(info GCC MAJ VER $(GCCMAJ)) +#$(info GCC MIN VER $(GCCMIN)) +#$(info GCC PAT VER $(GCCPAT)) + +#Project specific variables +EDDYBUILDPARAMETERS = "cuda=1 CUDAVER=9.1" "cuda=1 CUDAVER=9.1" "cpu=1" +fdt_MASTERBUILD = COMPILE_GPU = 0 +ptx2_MASTERBUILD = COMPILE_GPU = 0 +define newline + + +endef +#PTX2_MASTER_COMMANDS = COMPILE_GPU = 1$(newline)FOO=2 +endif # if Linux + diff --git a/externallibs.mk b/externallibs.mk deleted file mode 100755 index e1c5e44798c7..000000000000 --- a/externallibs.mk +++ /dev/null @@ -1,77 +0,0 @@ -# $Id: externallibs.mk,v 1.3 2018/10/10 11:12:48 mwebster Exp $ - -# External Library and Include Paths - -FSLEXTLIB=${FSLDIR}/extras/lib -FSLEXTINC=${FSLDIR}/extras/include -FSLEXTBIN=${FSLDIR}/extras/bin - -# GD library -LIB_GD = /usr/lib -INC_GD = /usr/include - -# GDC library -LIB_GDC = ${FSLEXTLIB} -INC_GDC = ${FSLEXTINC}/libgdc - -# LIBXML2 library -INC_XML2 = /usr/include/libxml2 - -# LIBXML++ library -INC_XML++ = /usr/include/libxml++-2.6 -INC_XML++CONF = /usr/lib/libxml++-2.6/include - -# GSL library -LIB_GSL = /usr/lib -INC_GSL = /usr/include/gsl - -# PNG library -LIB_PNG = /usr/lib -INC_PNG = /usr/include/libpng1.6 - -# PROB library -LIB_PROB = ${FSLEXTLIB} -INC_PROB = ${FSLEXTINC}/libprob - -# CPROB library -LIB_CPROB = ${FSLEXTLIB} -INC_CPROB = ${FSLEXTINC}/libcprob - -# NEWMAT library -#LIB_NEWMAT = ${FSLEXTLIB} -llapack -lblas or just -lopenblas -#LIB_NEWMAT = /usr/lib/newmat -#INC_NEWMAT = /usr/include/newmat -LIB_NEWMAT = ${FSLEXTLIB} -llapack -lblas -INC_NEWMAT = ${FSLEXTINC}/armawrap/armawrap -DARMA_USE_LAPACK -DARMA_USE_BLAS -DARMA_64BIT_WORD - -# NEWRAN library -LIB_NEWRAN = ${FSLEXTLIB} -INC_NEWRAN = ${FSLEXTINC}/newran - -# ZLIB library -LIB_ZLIB = /usr/lib -INC_ZLIB = /usr/include - -# BOOST library -#BOOSTDIR = ${FSLEXTINC}/boost -LIB_BOOST = /usr/lib -INC_BOOST = /usr/include/boost - -# QT library -#QTDIR = /usr/lib/qt5 -LIB_QT = /usr/lib -INC_QT = ${QTDIR}/include/qt - -# QWT library -#QWTDIR = /usr/local/qwt -LIB_QWT = /usr/lib -INC_QWT = /usr/include/qwt - -# FFTW3 library -LIB_FFTW3 = /usr/lib -INC_FFTW3 = /usr/include - -# VTK library -VTKDIR_INC = /usr/include/vtk -VTKDIR_LIB = /usr/lib -VTKSUFFIX = diff --git a/fsl_exec.patch b/fsl_exec.patch deleted file mode 100644 index 3a5628c37044..000000000000 --- a/fsl_exec.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- fsl/src/misc_tcl/fsl_exec.tcl.orig 2017-06-12 17:54:14.000000000 -0400 -+++ fsl/src/misc_tcl/fsl_exec.tcl 2017-06-12 18:45:07.176756012 -0400 -@@ -175,6 +175,7 @@ proc fsl:exec { thecommand args } { - set logout "" - } - -+ if { ! [ info exists ::errorCode ] } { set ::errorCode "NONE" } - # run and log the actual command - if { $do_logout } { - fsl:echo $logout "\n$thecommand" - diff --git a/fsl_sub b/fsl_sub new file mode 100644 index 000000000000..cf74a2795289 --- /dev/null +++ b/fsl_sub @@ -0,0 +1,544 @@ +#!/bin/bash + +# Copyright (C) 2007-2017 University of Oxford +# Authors: Dave Flitney, Stephen Smith, Matthew Webster and Duncan Mortimer + +# Part of FSL - FMRIB's Software Library +# http://www.fmrib.ox.ac.uk/fsl +# fsl@fmrib.ox.ac.uk +# +# Developed at FMRIB (Oxford Centre for Functional Magnetic Resonance +# Imaging of the Brain), Department of Clinical Neurology, Oxford +# University, Oxford, UK +# +# +# LICENCE +# +# FMRIB Software Library, Release 6.0 (c) 2018, The University of +# Oxford (the "Software") +# +# The Software remains the property of the Oxford University Innovation +# ("the University"). +# +# The Software is distributed "AS IS" under this Licence solely for +# non-commercial use in the hope that it will be useful, but in order +# that the University as a charitable foundation protects its assets for +# the benefit of its educational and research purposes, the University +# makes clear that no condition is made or to be implied, nor is any +# warranty given or to be implied, as to the accuracy of the Software, +# or that it will be suitable for any particular purpose or for use +# under any specific conditions. Furthermore, the University disclaims +# all responsibility for the use which is made of the Software. It +# further disclaims any liability for the outcomes arising from using +# the Software. +# +# The Licensee agrees to indemnify the University and hold the +# University harmless from and against any and all claims, damages and +# liabilities asserted by third parties (including claims for +# negligence) which arise directly or indirectly from the use of the +# Software or the sale of any products based on the Software. +# +# No part of the Software may be reproduced, modified, transmitted or +# transferred in any form or by any means, electronic or mechanical, +# without the express permission of the University. The permission of +# the University is not required if the said reproduction, modification, +# transmission or transference is done without financial return, the +# conditions of this Licence are imposed upon the receiver of the +# product, and all original and amended source code is included in any +# transmitted product. You may be held legally responsible for any +# copyright infringement that is caused or encouraged by your failure to +# abide by these terms and conditions. +# +# You are not permitted under this Licence to use this Software +# commercially. Use for which any financial return is received shall be +# defined as commercial use, and includes (1) integration of all or part +# of the source code or the Software into a product for sale or license +# by or on behalf of Licensee to third parties or (2) use of the +# Software or any derivative of it for research with the final aim of +# developing software products for sale or license to a third party or +# (3) use of the Software or any derivative of it for research with the +# final aim of developing non-software products for sale or license to a +# third party, or (4) use of the Software to provide any service to an +# external organisation for which payment is received. If you are +# interested in using the Software commercially, please contact Oxford +# University Innovation ("OUI"), the technology transfer company of the +# University, to negotiate a licence. Contact details are: +# fsl@innovation.ox.ac.uk quoting Reference Project 9564, FSL. +export LC_ALL=C +export LC_ALL=C + +########################################################################### +# Edit this file in order to setup FSL to use your local compute +# cluster. +########################################################################### +set +o errexit + +########################################################################### +# The following section determines what to do when fsl_sub is called +# by an FSL program. If SGE_ROOT is set it will attempt to pass the +# commands onto the cluster, otherwise it will run the commands +# itself. There are two values for the METHOD variable, "SGE" and +# "NONE". Note that a user can unset SGE_ROOT if they don't want the +# cluster to be used. +########################################################################### +METHOD=SGE +unset module +if [[ "x$SGE_ROOT" = "x" ]] ; then + METHOD=NONE +else + QCONF=$(which qconf) + if [[ "x$QCONF" = "x" ]]; then + METHOD=NONE + echo "Warning: SGE_ROOT environment variable is set but Grid Engine software not found, will run locally" >&2 + fi +fi + +# stop submitted scripts from submitting jobs themselves +if [[ "X$FSLSUBALREADYRUN" = "Xtrue" ]] ; then + METHOD=NONE + echo "Warning: job on queue attempted to submit parallel jobs - running jobs serially instead" >&2 +fi + +if [[ "X$METHOD" = "XNONE" ]]; then + QCONF=echo +fi +FSLSUBALREADYRUN=true +export FSLSUBALREADYRUN + +########################################################################### +# The following auto-decides what cluster queue to use. The calling +# FSL program will probably use the -T option when calling fsl_sub, +# which tells fsl_sub how long (in minutes) the process is expected to +# take (in the case of the -t option, how long each line in the +# supplied file is expected to take). You need to setup the following +# list to map ranges of timings into your cluster queues - it doesn't +# matter how many you setup, that's up to you. +########################################################################### + +map_qname () +{ + if [[ "$1" -le 20 ]] ; then + queue=veryshort.q + elif [[ "$1" -le 120 ]] ; then + queue=short.q + elif [[ "$1" -le 1440 ]] ; then + queue=long.q + else + queue=verylong.q + fi + queueCmd=" -q $queue " + + #echo "Estimated time was $1 mins: queue name is $queue" +} + +########################################################################### +# Don't change the following (but keep scrolling down!) +########################################################################### + +if [[ ! -z "${POSIXLY_CORRECT}" ]]; then + OLD_POSIXLY_CORRECT=${POSIXLY_CORRECT} +fi + +POSIXLY_CORRECT=1 +export POSIXLY_CORRECT +command=$(basename "$0") + +usage () +{ + cat <<EOF + +$command V1.1 - wrapper for job control system such as SGE + +Usage: $command [options] <command> + +$command gzip *.img *.hdr +$command -q short.q gzip *.img *.hdr +$command -a darwin regscript rawdata outputdir ... + + -T <minutes> Estimated job length in minutes, used to auto-set queue name + -q <queuename> Possible values for <queuename> are "verylong.q", "long.q" + and "short.q". See below for details + Default is "long.q". + -a <arch-name> Architecture [e.g., darwin or lx24-amd64] + -p <job-priority> Lower priority [0:-1024] default = 0 + -M <email-address> Who to email, default = $(whoami)@$(hostname -f | cut -d . -f 2-) + -j <jid> Place a hold on this task until job jid has completed + -t <filename> Specify a task file of commands to execute in parallel + -N <jobname> Specify jobname as it will appear on queue + -R <RAM> Max total RAM to use for job (integer in MB) + -l <logdirname> Where to output logfiles + -m <mailoptions> Change the SGE mail options, see qsub for details + -z <output> If <output> image or file already exists, do nothing and exit + -F Use flags embedded in scripts to set SGE queuing options + -s <pename>,<threads> Submit a multi-threaded task - requires a PE (<pename>) to be + configured for the requested queues. + <threads> specifies the number of threads to run + -v Verbose mode. + +Queues: + +There are several batch queues configured on the cluster, each with defined CPU +time limits. All queues, except bigmem.q, have a 8GB memory limit. + +veryshort.q:This queue is for jobs which last under 30mins. +short.q: This queue is for jobs which last up to 4h. +long.q: This queue is for jobs which last less than 24h. Jobs run with a + nice value of 10. +verylong.q: This queue is for jobs which will take longer than 24h CPU time. + There is one slot per node, and jobs on this queue have a nice value + of 15. +bigmem.q: This queue is like the verylong.q but has no memory limits. + +EOF + + exit 1 +} + +nargs=$# +if [[ "$nargs" -eq 0 ]] ; then + usage +fi + +#if the newer whitespace-safe getopt format is available, use it +if [[ $(getopt -T >/dev/null 2>&1; echo $?) == 4 ]]; then + eval set -- $(getopt -s bash T:q:a:p:M:j:t:z:N:R:Fvm:l:s: "$@") + result=$? +else + set -- $(getopt T:q:a:p:M:j:t:z:N:R:Fvm:l:s: "$@") + result=$? +fi +if [[ "$result" != 0 ]] ; then + echo "What? Your arguments make no sense!" +fi + +if [[ "$nargs" -eq 0 ]] || [[ $result != 0 ]] ; then + usage +fi + +if [[ -z "${OLD_POSIXLY_CORRECT}" ]]; then + unset POSIXLY_CORRECT +else + POSIXLY_CORRECT=${OLD_POSIXLY_CORRECT} + export POSIXLY_CORRECT +fi + +########################################################################### +# If you have a Parallel Environment configured for OpenMP tasks then +# the variable omp_pe should be set to the name you have defined for that +# PE. The script will work out which queues have that PE setup on them. +# Note, we support openmp tasks even when Grid Engine is not in use. +########################################################################### + +omp_pe='openmp' + + +########################################################################### +# If you wish to disable processor affinities under Grid Engine then +# comment the following line. +# This instructs Grid Engine to bind the task to the number of job slots +# allocated to the job (or PE) +########################################################################### +proc_affinities="-binding linear:slots" + + +########################################################################### +# The following sets up the default queue name, which you may want to +# change. It also sets up the basic emailing control. +########################################################################### + +queue=long.q +queueCmd=" -q long.q " +mailto=$(whoami)@$(hostname -f | cut -d . -f 2-) +MailOpts="a" + + +########################################################################### +# In the following, you might want to change the behaviour of some +# flags so that they prepare the right arguments for the actual +# cluster queue submission program, in our case "qsub". +# +# -a sets is the cluster submission flag for controlling the required +# hardware architecture (normally not set by the calling program) +# +# -p set the priority of the job - ignore this if your cluster +# environment doesn't have priority control in this way. +# +# -j tells the cluster not to start this job until cluster job ID $jid +# has completed. You will need this feature. +# +# -t will pass on to the cluster software the name of a text file +# containing a set of commands to run in parallel; one command per +# line. +# +# -N option determines what the command will be called when you list +# running processes. +# +# -l tells the cluster what to call the standard output and standard +# -error logfiles for the submitted program. +########################################################################### + +if [[ -z "$FSLSUBVERBOSE" ]] ; then + verbose=0 +else + verbose=$FSLSUBVERBOSE; + echo "METHOD=$METHOD : args=$*" >&2 +fi + +scriptmode=0 + +while [[ "$1" != -- ]] ; do + case "$1" in + -z) + if [[ -e "$2" || $("${FSLDIR}/bin/imtest" "$2") = 1 ]] ; then + exit 0 + fi + shift;; + -T) + map_qname "$2" + shift;; + -q) + queue="$2" + queueCmd=" -q $queue " + "$QCONF" -sq "$queue" >/dev/null 2>&1 + if [[ $? -eq 1 ]]; then + echo "Invalid queue specified!" + exit 127 + fi + shift;; + -a) + acceptable_arch=no + available_archs=$(qhost | tail -n +4 | awk '{print $2}' | sort | uniq) + for a in $available_archs; do + if [[ "$2" = "$a" ]] ; then + acceptable_arch="yes" + fi + done + if [[ "$acceptable_arch" = "yes" ]]; then + sge_arch="-l arch=$2" + else + echo "Sorry arch of $2 is not supported on this SGE configuration!" + echo "Should be one of: $available_archs" + exit 127 + fi + shift;; + -p) + # Not implmented + shift;; + -M) + mailto=$2 + shift;; + -j) + jid=$2 + sge_hold="-hold_jid $jid" + shift;; + -t) + taskfile=$2 + if [[ -f "$taskfile" ]] ; then + tasks=$(wc -l "$taskfile" | awk '{print $1}') + if [[ "$tasks" -ne 0 ]]; then + sge_tasks="-t 1-$tasks" + else + echo "Task file ${taskfile} is empty" + echo "Should be a text file listing all the commands to run!" + exit -1 + fi + else + echo "Task file (${taskfile}) does not exist" + exit -1 + fi + shift;; + -N) + JobName=$2; + shift;; + -R) + RAM="-l mem_free=${2}M" + shift;; + -m) + MailOpts=$2; + shift;; + -l) + LogOpts="-o $2 -e $2"; + LogDir="${2}/"; + if [[ ! -e "${2}" ]]; then + mkdir -p "$2" + else + echo "${2}" | grep '/dev/null' >/dev/null 2>&1 + if [[ $? -eq 1 ]] && [[ -f "${2}" ]]; then + echo "Log destination is a file (should be a folder)" + exit -1 + fi + fi + shift;; + -F) + scriptmode=1; + ;; + -v) + verbose=1 + ;; + -s) + pe_string=$2; + peName=$(echo "$pe_string" | cut -d',' -f 1) + peThreads=$(echo "$pe_string" | cut -d',' -f 2) + shift;; + esac + shift # next flag +done +shift + +########################################################################### +# Don't change the following (but keep scrolling down!) +########################################################################### +commandline=("$@") +command="${commandline[0]}" + +if [[ -z "$taskfile" ]] && [[ -z "$command" ]]; then + echo "Either supply a command to run or a parallel task file" + exit -1 +fi + +if [[ -z "$taskfile" ]] && [[ ! -x "$command" ]]; then + which "$command" >/dev/null 2>&1 + if [[ $? -ne 0 ]]; then + echo "The command you have requested cannot be found or is not executable" + exit -1 + fi +fi + +if [[ "x$JobName" = x ]] ; then + if [[ "x$taskfile" != x ]] ; then + JobName=$(basename "$taskfile") + else + JobName=$(basename "$command") + fi +fi + +if [[ -n "$tasks" ]] && [[ -n "${commandline[*]}" ]] ; then + echo "Spurious input after parsing command line: \"${commandline[*]}\"!" + echo "You appear to have specified both a task file and a command to run" + exit -1 +fi + +if [[ -n "$peName" ]]; then + # If the PE name is 'openmp' then limit the number of threads to those specified + if [[ "X$peName" = "X$omp_pe" ]]; then + OMP_NUM_THREADS=$peThreads + export OMP_NUM_THREADS + fi +fi + +case "$METHOD" in + +########################################################################### +# The following is the main call to the cluster, using the "qsub" SGE +# program. If $tasks has not been set then qsub is running a single +# command, otherwise qsub is processing a text file of parallel +# commands. +########################################################################### + + SGE) + ########################################################################### + # Test Parallel environment options + ########################################################################### + if [[ -n "$peName" ]]; then + # Is this a configured PE? + + "$QCONF" -sp "$peName" >/dev/null 2>&1 + + if [[ $? -eq 1 ]]; then + echo "${commandline[*]}" + echo "$peName is not a valid PE" + exit -1 + fi + + # Get a list of queues configured for this PE and confirm that the queue + # we have submitted to has that PE set up. + qstat -g c -pe "$peName" >/dev/null 2>&1 + if [[ $? -eq 1 ]]; then + echo "No parallel environments configured!" + exit -1 + fi + + qstat -g c -pe "$peName" | sed '1,2d' | awk '{ print $1 }' | grep "^$queue" >/dev/null 2>&1 + + if [[ $? -eq 1 ]]; then + echo "${commandline[*]}" + echo "PE $peName is not configured on $queue" + exit -1 + fi + + # The -w e option will result in the job failing if there are insufficient slots + # on any of the cluster nodes + pe_options="-pe $peName $peThreads -w e" + fi + + if [[ -z "$tasks" ]] ; then + if [[ "$scriptmode" -ne 1 ]] ; then + sge_command=(qsub -V -cwd -shell n -b y -r y $queueCmd $proc_affinities $pe_options -M $mailto -N "$JobName" -m $MailOpts $LogOpts $sge_arch $RAM $sge_hold) + else + sge_command=(qsub $proc_affinities $LogOpts $sge_arch $sge_hold) + fi + if [[ "$verbose" -eq 1 ]] ; then + echo "sge_command: ${sge_command[*]}" >&2 + echo "executing: ${commandline[*]}" >&2 + fi + exec "${sge_command[@]}" "${commandline[@]}" | awk '{print $3}' + else + sge_command=(qsub -V -cwd $queueCmd $proc_affinities $pe_options -M $mailto -N "$JobName" -m $MailOpts $LogOpts $sge_arch $RAM $sge_hold $sge_tasks) + if [[ "$verbose" -eq 1 ]] ; then + echo "sge_command: ${sge_command[*]}" >&2 + echo "control file: $taskfile" >&2 + fi + exec "${sge_command[@]}" <<EOF | awk '{print $3}' | awk -F. '{print $1}' +#!/bin/sh + +#$ -S /bin/sh + +command=\`sed -n -e "\${SGE_TASK_ID}p" $taskfile\` + +exec /bin/sh -c "\$command" +EOF + fi + ;; + +########################################################################### +# Don't change the following - this runs the commands directly if a +# cluster is not being used. +########################################################################### + + NONE) + if [[ "x$tasks" = "x" ]] ; then + if [[ "$verbose" -eq 1 ]] ; then + echo executing: "${commandline[*]}" >&2 + fi + + "${commandline[@]}" > "${LogDir}${JobName}.o$$" 2> "${LogDir}${JobName}.e$$" + ERR=$? + if [[ "$ERR" -ne 0 ]] ; then + cat "${LogDir}${JobName}.e$$" >&2 + exit $ERR + fi + else + if [[ "$verbose" -eq 1 ]] ; then + echo "Running commands in: $taskfile" >&2 + fi + + n=1 + while [[ "$n" -le "$tasks" ]] ; do + line=$(sed -n -e ''${n}'p' "$taskfile") + if [[ "$verbose" -eq 1 ]] ; then + echo "executing: $line" >&2 + fi + /bin/sh <<EOF2 > "${LogDir}${JobName}.o$$.$n" 2> "${LogDir}${JobName}.e$$.$n" +$line +EOF2 + n=$((n+1)) + done + fi + echo $$ + ;; + +esac + +########################################################################### +# Done. +########################################################################### diff --git a/fslinstaller.py b/fslinstaller.py new file mode 100644 index 000000000000..a5d01121f392 --- /dev/null +++ b/fslinstaller.py @@ -0,0 +1,2892 @@ +#!/usr/bin/env python +# +# SHBASECOPYRIGHT +# +# FSL installer script. +# +"""This is the FSL installation script, which can be used to install FSL. + +This script must: + + - be able to be executed with Python 2.7 or newer. + + - be able to be executed in a "vanilla" Python environment, with no third + party dependencies. + + - be self-contained, with no dependencies on any other modules (apart from + the Python standard library). + + - be importable as a Python module - this script contains functions and + classes that may be used by other scripts. +""" + + +from __future__ import print_function, division, unicode_literals + +import functools as ft +import os.path as op +import subprocess as sp +import textwrap as tw +import argparse +import contextlib +import datetime +import fnmatch +import getpass +import glob +import hashlib +import json +import logging +import os +import platform +import pwd +import readline +import shlex +import shutil +import ssl +import sys +import tempfile +import threading +import time +import traceback + +try: + import urllib.request as urlrequest +except ImportError: + import urllib + import urllib2 as urlrequest + urlrequest.pathname2url = urllib.pathname2url + + +try: import urllib.parse as urlparse +except ImportError: import urlparse + +try: import queue +except ImportError: import Queue as queue + + +PYVER = sys.version_info[:2] + + +log = logging.getLogger(__name__) + + +# this sometimes gets set to fslinstaller.pyc, so rstrip c +__absfile__ = op.abspath(__file__).rstrip('c') + + +__version__ = '3.9.0' +"""Installer script version number. This must be updated +whenever a new version of the installer script is released. +""" + + +DEFAULT_INSTALLATION_DIRECTORY = op.join(op.expanduser('~'), 'fsl') +"""Default FSL installation directory. """ + + +DEFAULT_ROOT_INSTALLATION_DIRECTORY = '/usr/local/fsl/' +"""Default FSL installation directory when the installer is run as root. """ + + +FSL_RELEASE_MANIFEST = 'https://fsl.fmrib.ox.ac.uk/fsldownloads/' \ + 'fslconda/releases/manifest.json' +"""URL to download the FSL installer manifest file from. The installer +manifest file is a JSON file which contains information about available FSL +versions. + +See the download_manifest function, and an example manifest file +in test/data/manifest.json, for more details. + +A custom manifest URL can be specified with the -a/--manifest command-line +option. +""" + + +FSL_DEV_RELEASES = 'https://fsl.fmrib.ox.ac.uk/fsldownloads/' \ + 'fslconda/releases/devreleases.txt' +"""URL to the devreleases.txt file, which contains a list of available +internal/development FSL releases. See the download_dev_releases function +for more details. +""" + + +# List of modifiers which can be used to change how +# a message is printed by the printmsg function. +INFO = 1 +IMPORTANT = 2 +QUESTION = 3 +PROMPT = 4 +WARNING = 5 +ERROR = 6 +EMPH = 7 +EMPHASIS = 7 +UNDERLINE = 8 +RESET = 9 +ANSICODES = { + INFO : '\033[37m', # Light grey + IMPORTANT : '\033[92m', # Green + QUESTION : '\033[36m\033[4m', # Blue+underline + PROMPT : '\033[36m\033[1m', # Bright blue+bold + WARNING : '\033[93m', # Yellow + ERROR : '\033[91m', # Red + EMPHASIS : '\033[1m', # White+bold + UNDERLINE : '\033[4m', # Underline + RESET : '\033[0m', # Used internally +} + +def get_terminal_width(fallback=None): + """Return the number of columns in the current terminal, or fallback + if it cannot be determined. + """ + # os.get_terminal_size added in python + # 3.3, so we try it but fall back to + # COLUMNS, or tput as a last resort. + try: + return shutil.get_terminal_size()[0] + except Exception: + pass + + try: + return int(os.environ['COLUMNS']) + except Exception: + pass + + try: + result = Process.check_output('tput cols', log_output=False) + return int(result.strip()) + except Exception: + return fallback + + +def printmsg(*args, **kwargs): + """Prints a sequence of strings formatted with ANSI codes. Expects + positional arguments to be of the form:: + + printable, ANSICODE, printable, ANSICODE, ... + + :arg log: Must be specified as a keyword argument. If True (default), + the message is logged. + + :arg fill: Must be specified as a keyword argument. If True (default), + the message is wrapped to the terminal width. + + All other keyword arguments are passed through to the print function. + """ + + args = list(args) + blockids = [i for i in range(len(args)) if (args[i] not in ANSICODES)] + logmsg = kwargs.pop('log', True) + fill = kwargs.pop('fill', True) + + coded = '' + uncoded = '' + + for i, idx in enumerate(blockids): + if i == len(blockids) - 1: + slc = slice(idx + 1, None) + else: + slc = slice(idx + 1, blockids[i + 1]) + + msg = args[idx] + msgcodes = args[slc] + msgcodes = [ANSICODES[c] for c in msgcodes] + msgcodes = ''.join(msgcodes) + uncoded += msg + coded += '{}{}{}'.format(msgcodes, msg, ANSICODES[RESET]) + + if len(blockids) > 0: + + if fill: + width = get_terminal_width(70) + coded = tw.fill(coded, width, replace_whitespace=False) + + print(coded, **kwargs) + + if logmsg: + log.debug(uncoded) + + sys.stdout.flush() + + +def prompt(promptmsg, *msgtypes, **kwargs): + """Prompts the user for some input. msgtypes and kwargs are passed + through to the printmsg function. + """ + printmsg(promptmsg, *msgtypes, end='', log=False, **kwargs) + + if PYVER[0] == 2: response = raw_input(' ').strip() + else: response = input( ' ').strip() + + log.debug('%s: %s', promptmsg, response) + + return response + + +def post_request(url, data): + """Send JSON data to a URL via a HTTP POST request. """ + + data = json.dumps(data).encode('utf-8') + headers = {} + headers['Content-Type'] = 'application/json' + resp = None + + try: + req = urlrequest.Request(url, + headers=headers, + data=data) + resp = urlrequest.urlopen(req) + finally: + if resp: + resp.close() + + +def identify_platform(): + """Figures out what platform we are running on. Returns a platform + identifier string - one of: + + - "linux-64" (Linux, x86_64) + - "macos-64" (macOS, x86_64) + - "macos-M1" (macOS, M1) + + Note that these identifiers are for FSL releases, and are not the + same as the platform identifiers used by conda. + """ + + platforms = { + ('linux', 'x86_64') : 'linux-64', + ('darwin', 'x86_64') : 'macos-64', + ('darwin', 'arm64') : 'macos-M1', + } + + system = platform.system().lower() + cpu = platform.machine() + key = (system, cpu) + + if key not in platforms: + supported = ', '.join(['[{}, {}]' for s, c in platforms]) + raise Exception('This platform [{}, {}] is unrecognised or ' + 'unsupported! Supported platforms: {}'.format( + system, cpu, supported)) + + return platforms[key] + + +def timestamp(): + """Return a string containing the local time, with time zone offset. + """ + now = datetime.datetime.now() + offset = (now - datetime.datetime.utcnow()) + offset = round(offset.total_seconds()) + hours = int(offset / 3600) + minutes = int((offset % 3600) / 60) + now = now.strftime('%Y-%m-%dT%H:%M:%S') + return '{}{:+03d}:{:02d}'.format(now, hours, minutes) + + +def check_need_admin(dirname): + """Returns True if dirname needs administrator privileges to write to, + False otherwise. + """ + # os.supports_effective_ids added in + # python 3.3, so can't be used here + return not os.access(dirname, os.W_OK | os.X_OK) + + +def get_admin_password(action='install FSL'): + """Prompt the user for their administrator password. An Exception is raised + if an incorrect password is entered three times.a + + :arg action: String which describes what the password is needed for, i.e.: + "Your administrator password is needed to {action}" + :returns: the validated administrator password + """ + + def validate_admin_password(password): + proc = Process.sudo_popen(['true'], password, stdin=sp.PIPE) + proc.communicate() + return proc.returncode == 0 + + msg = 'Your administrator password is needed to {}'.format(action) + + for attempt in range(3): + if attempt == 0: msg = '{}:'.format(msg) + else: msg = '{} [attempt {} of 3]:'.format(msg, attempt + 1) + printmsg(msg, IMPORTANT, end='') + password = getpass.getpass('') + valid = validate_admin_password(password) + + if valid: + printmsg('Password accepted', INFO) + break + else: + printmsg('Incorrect password', WARNING) + + if not valid: + raise Exception('Incorrect password') + + return password + + +def isstr(s): + """Returns True if s is a string, False otherwise. Works on python 2.7 + and >=3.3. + """ + try: return isinstance(s, basestring) + except Exception: return isinstance(s, str) + + +def match_any(s, patterns): + """Test if the string s matches any of the fnmatch-style patterns. + Returns the matched pattern, or None. + """ + for pat in patterns: + if fnmatch.fnmatch(s, pat): + return pat + return None + + +@contextlib.contextmanager +def tempdir(override_dir=None, change_into=True, delete=True): + """Returns a context manager which creates, changes into, and returns a + temporary directory, and then deletes it on exit (unless delete is False). + + If override_dir is not None, instead of creating and changing into a + temporary directory, this function just changes into override_dir. + """ + + if override_dir is None: tmpdir = tempfile.mkdtemp() + else: tmpdir = override_dir + + prevdir = os.getcwd() + + try: + if change_into: + os.chdir(tmpdir) + yield tmpdir + + finally: + if change_into: + os.chdir(prevdir) + if delete and override_dir is None: + shutil.rmtree(tmpdir) + + +def warn_on_error(*msgargs, **msgkwargs): + """Decorator which tries to run a function, and prints a message if it + fails. The arguments after the function are passed to the printmsg + function, e.g.: + + @warn_on_error('Function failed!', WARNING) + def function(a, b): + ... + + function('a', 'b') + + :arg toscreen: Defaults to True. Print the warning to the screen. + :arg tolog: Defaults to True. Print the warning to the log file. + """ + + toscreen = msgkwargs.pop('toscreen', True) + tolog = msgkwargs.pop('tolog', True) + + def decorator(function): + def wrapper(*args, **kwargs): + try: + function(*args, **kwargs) + except Exception as e: + if toscreen: printmsg(*msgargs, **msgkwargs) + if tolog: log.debug('%s', e, exc_info=True) + return wrapper + return decorator + + +@contextlib.contextmanager +def tempfilename(permissions=None, delete=True): + """Returns a context manager which creates a temporary file, yields its + name, then deletes the file on exit. + """ + + fname = None + + try: + tmpf = tempfile.NamedTemporaryFile(delete=False) + fname = tmpf.name + + tmpf.close() + + if permissions: + os.chmod(fname, permissions) + + yield fname + + finally: + if delete and fname and op.exists(fname): + os.remove(fname) + + +def sha256(filename, check_against=None, blocksize=1048576): + """Calculate the SHA256 checksum of the given file. If check_against + is provided, it is compared against the calculated checksum, and an + error is raised if they are not the same. + """ + + hashobj = hashlib.sha256() + + with open(filename, 'rb') as f: + while True: + block = f.read(blocksize) + if len(block) == 0: + break + hashobj.update(block) + + checksum = hashobj.hexdigest() + + if check_against is not None: + if checksum != check_against: + raise Exception('File {} does not match expected checksum ' + '({})'.format(filename, check_against)) + + return checksum + + +def clean_environ(): + """Return a dict containing a set of sanitised environment variables. + + All FSL and conda related variables are removed. + """ + env = os.environ.copy() + for v in list(env.keys()): + if any(('FSL' in v, 'CONDA' in v, 'MAMBA' in v, 'PYTHON' in v)): + env.pop(v) + return env + + +def install_environ(fsldir, username=None, password=None): + """Returns a dict containing some environment variables that should + be added to the shell environment when the FSL conda environment is + being installed. + """ + env = {} + # post-link scripts call $FSLDIR/share/fsl/sbin/createFSLWrapper + # (part of fsl/base), which will only do its thing if the following + # env vars are set + env['FSL_CREATE_WRAPPER_SCRIPTS'] = '1' + env['FSLDIR'] = fsldir + + # Make sure HTTP proxy variables, if set, + # are available to the conda env command + for v in ['http_proxy', 'https_proxy', + 'HTTP_PROXY', 'HTTPS_PROXY']: + if v in os.environ: + env[v] = os.environ[v] + + # Tell mamba not to abort if the download is taking time + # https://github.com/mamba-org/mamba/issues/1941 + env['MAMBA_NO_LOW_SPEED_LIMIT'] = '1' + + # FSL environments which source packages from the internal + # FSL conda channel will refer to the channel as: + # + # http://${FSLCONDA_USERNAME}:${FSLCONDA_PASSWORD}/abc.com/ + # + # so we need to set those variables + if username: env['FSLCONDA_USERNAME'] = username + if password: env['FSLCONDA_PASSWORD'] = password + + return env + + +def download_file(url, + destination, + progress=None, + blocksize=131072, + ssl_verify=True): + """Download a file from url, saving it to destination. """ + + def default_progress(downloaded, total): + pass + + if progress is None: + progress = default_progress + + log.debug('Downloading %s ...', url) + + # Path to local file + if op.exists(url): + url = 'file:' + urlrequest.pathname2url(op.abspath(url)) + + # We create and use an unconfigured SSL + # context to disable SSL verification. + # Otherwise pass None causes urlopen to + # use default behaviour. + kwargs = {} + if not ssl_verify: + + # - The urlopen(context) argument is not available in py3.3 + # - py3.4 does not have PROTOCOL_TLS + # - PROTOCOL_TLS deprecated in py3.10 + if PYVER == (3, 3): pro = None + elif hasattr(ssl, 'PROTOCOL_TLS_CLIENT'): pro = ssl.PROTOCOL_TLS_CLIENT + elif hasattr(ssl, 'PROTOCOL_TLS'): pro = ssl.PROTOCOL_TLS + elif hasattr(ssl, 'PROTOCOL_TLSv1_2'): pro = ssl.PROTOCOL_TLSv1_2 + elif hasattr(ssl, 'PROTOCOL_TLSv1_1'): pro = ssl.PROTOCOL_TLSv1_1 + elif hasattr(ssl, 'PROTOCOL_TLSv1'): pro = ssl.PROTOCOL_TLSv1 + else: pro = None + + if pro is None: + printmsg('SSL verification cannot be skipped - if this is ' + 'a problem, try running the installer with a newer ' + 'version of Python.', INFO) + else: + printmsg('Skipping SSL verification - this ' + 'is not recommended!', WARNING) + + sslctx = ssl.SSLContext(pro) + sslctx.check_hostname = False + sslctx.verify_mode = ssl.CERT_NONE + kwargs['context'] = sslctx + + req = None + + try: + # py2: urlopen result cannot be + # used as a context manager + req = urlrequest.urlopen(url, **kwargs) + with open(destination, 'wb') as outf: + + try: total = int(req.headers['content-length']) + except KeyError: total = None + + downloaded = 0 + + progress(downloaded, total) + while True: + block = req.read(blocksize) + if len(block) == 0: + break + downloaded += len(block) + outf.write(block) + progress(downloaded, total) + + finally: + if req: + req.close() + + +def download_manifest(url, workdir=None, **kwargs): + """Downloads the installer manifest file, which contains information + about available FSL versions, and the most recent version number of the + installer (this script). + + Keyword arguments are passed through to the download_file function. + + The manifest file is a JSON file. Lines beginning with a double-forward + slash are ignored. + + This function modifies the manifest structure by adding a 'version' + attribute to all FSL build entries. + """ + + log.debug('Downloading FSL installer manifest from %s', url) + + with tempdir(workdir): + + try: + download_file(url, 'manifest.json', **kwargs) + except Exception as e: + log.debug('Error downloading FSL release manifest from %s', + url, exc_info=True) + raise Exception('Unable to download FSL release manifest ' + 'from {} [{}]!'.format(url, str(e))) + + with open('manifest.json') as f: + lines = f.readlines() + + # Drop comments + lines = [l for l in lines if not l.lstrip().startswith('//')] + + manifest = json.loads('\n'.join(lines)) + + # Add "version" to every build + for version, builds in manifest['versions'].items(): + if version == 'latest': + continue + for build in builds: + build['version'] = version + + return manifest + + +def download_dev_releases(url, workdir=None, **kwargs): + """Downloads the FSL_DEV_RELEASES file. This file contains a list of + available development manifest URLS. Returns a list of tuples, one + for each development release, with each tuple containing: + + - URL to the manifest file + - Version identifier + - Commit hash (on the fsl/conda/manifest repository) + - Branch name (on the fsl/conda/manifest repository) + + The list is sorted by date, newest first. + + Keyword arguments are passed through to the download_file function. + """ + + # parse a dev manifest file name, returning + # a sequence containing the tage, date, commit + # hash, and branch name. Dev manifest files + # are named like so: + # + # manifest-<tag>.<date>.<commit>.<branch>.json + # + # where <tag> is the tag of the most recent + # public FSL release, and everything else is + # self-explanatory. + def parse_devrelease_name(url): + name = urlparse.urlparse(url).path + name = op.basename(name) + name = name.lstrip('manifest-').rstrip('.json') + + # The devrelease list may contain public + # releases too - sniff the commit, and if + # it doesn't look like a commit hash, + # assume that this file corresponds to a + # public release. + commit = name.rsplit('.', 2)[-2] + + # public release or dev release + if len(commit) < 7: bits = [name, None, None] + else: bits = name.rsplit('.', 2) + + return bits + + # list of (url, version, commit, branch) + devreleases = [] + + with tempdir(workdir): + + try: + download_file(url, 'devreleases.txt', **kwargs) + except Exception as e: + log.debug('Error downloading devreleases.txt from %s', + url, exc_info=True) + raise Exception('Unable to download development manifest ' + 'list from {}!'.format(url)) + + with open('devreleases.txt', 'rt') as f: + urls = f.read().strip().split('\n') + urls = [l.strip() for l in urls] + + for url in urls: + devreleases.append([url] + parse_devrelease_name(url)) + + # sort by version, newest first + return sorted(devreleases, key=lambda r: Version(r[1]), reverse=True) + + +class Progress(object): + """Simple progress reporter. Displays one of the following: + + - If both a value and total are provided, a progress bar is shown + - If only a value is provided, a cumulative count is shown + - If nothing is provided, a spinner is shown. + + Use as a context manager, and call the update method to report progress, + e,g: + + with Progress('%') as p: + for i in range(100): + p.update(i + 1, 100) + """ + + def __init__(self, + label='', + transform=None, + fmt='{:.1f}', + total=None, + width=None, + proglabel='progress', + progfile=None): + """Create a Progress reporter. + + :arg label: Units (e.g. "MB", "%",) + + :arg transform: Function to transform values (see e.g. + Progress.bytes_to_mb) + + :arg fmt: Template string used to format value / total. + + :arg total: Maximum value - overrides the total value passed to + the update method. + + :arg width: Maximum width, if a progress bar is displayed. Default + is to automatically infer the terminal width (see + get_terminal_width). + + :arg proglabel: Label to use when writing progress updates to progfile. + + :arg progfile: File to write progress updates to. Each update is + written on a new line, and has the form: + + <proglabel> <value>[ <total>] + """ + + if transform is None: + transform = Progress.default_transform + + self.width = width + self.fmt = fmt.format + self.total = total + self.label = label + self.transform = transform + self.proglabel = proglabel + self.progfile = progfile + + # used by the spin function + self.__last_spin = None + + @staticmethod + def default_transform(val, total): + return val, total + + @staticmethod + def bytes_to_mb(val, total): + if val is not None: val = val / 1048576 + if total is not None: total = total / 1048576 + return val, total + + @staticmethod + def percent(val, total): + if val is None or total is None: + return val, total + return 100 * (val / total), 100 + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + printmsg('', log=False, fill=False) + + def write_progress(self, value, total): + + if self.progfile is None: + return + + if value is None: value = '' + if total is None: total = '' + + with open(self.progfile, 'at') as f: + f.write('{} {} {}\n'.format(self.proglabel, value, total)) + + def update(self, value=None, total=None): + + if total is None: + total = self.total + + value, total = self.transform(value, total) + + if value is None and total is None: + self.spin() + elif value is not None and total is None: + self.count(value) + elif value is not None and total is not None: + self.progress(value, total) + + self.write_progress(value, total) + + def spin(self): + + symbols = ['|', '/', '-', '\\'] + + if self.__last_spin is not None: last = self.__last_spin + else: last = symbols[-1] + + idx = symbols.index(last) + idx = (idx + 1) % len(symbols) + this = symbols[idx] + + printmsg(this, end='\r', log=False, fill=False) + self.__last_spin = this + + def count(self, value): + + value = self.fmt(value) + + if self.label is None: line = '{} ...'.format(value) + else: line = '{}{} ...'.format(value, self.label) + + printmsg(line, end='\r', log=False, fill=False) + + def progress(self, value, total): + + value = min(value, total) + + # arbitrary fallback of 50 columns if + # terminal width cannot be determined + if self.width is None: width = get_terminal_width(50) + else: width = self.width + + fvalue = self.fmt(value) + ftotal = self.fmt(total) + suffix = '{} / {} {}'.format(fvalue, ftotal, self.label).rstrip() + + # +5: - square brackets around bar + # - space between bar and tally + # - space+spin at the end + width = width - (len(suffix) + 5) + completed = int(round(width * (value / total))) + remaining = width - completed + progress = '[{}{}] {}'.format('#' * completed, + ' ' * remaining, + suffix) + + printmsg(progress, end='', log=False, fill=False) + printmsg(' ', end='', log=False, fill=False) + self.spin() + printmsg(end='\r', log=False, fill=False) + + +class Process(object): + """Container for a subprocess.Popen object, allowing non-blocking + line-based access to its standard output and error streams via separate + queues, while logging all outputs. + + Don't create a Process directly - use one of the following static methods: + - Process.check_output + - Process.check_call + - Process.monitor_progress + """ + + + def __init__(self, + cmd, + admin=False, + password=None, + log_output=True, + print_output=False, + append_env=None, + **kwargs): + """Run the specified command. Starts threads to capture stdout and + stderr. + + :arg cmd: Command to run - passed through shlex.split, then + passed to subprocess.Popen + + :arg admin: Run the command with administrative privileges + + :arg password: Administrator password - can be None if admin is + False. + + :arg log_output: If True, the command and all of its stdout/stderr + are logged. + + :arg print_output: If True, the command and all of its stdout/stderr + are logged. + + :arg append_env: Dictionary of additional environment to be set when + the command is run. + + :arg kwargs: Passed to subprocess.Popen + """ + + self.cmd = cmd + self.stdoutq = queue.Queue() + self.stderrq = queue.Queue() + + if log_output: + log.debug('Running %s [as admin: %s]', cmd, admin) + + self.popen = Process.popen(cmd, admin, password, + append_env=append_env, **kwargs) + + # threads for consuming stdout/stderr + self.stdout_thread = threading.Thread( + target=Process.forward_stream, + args=(self.popen.stdout, self.stdoutq, cmd, + 'stdout', log_output, print_output)) + self.stderr_thread = threading.Thread( + target=Process.forward_stream, + args=(self.popen.stderr, self.stderrq, cmd, + 'stderr', log_output, print_output)) + + self.stdout_thread.daemon = True + self.stderr_thread.daemon = True + self.stdout_thread.start() + self.stderr_thread.start() + + + def wait(self): + """Waits for the process to terminate, then waits for the stdout + and stderr consumer threads to finish. + """ + self.popen.wait() + self.stdout_thread.join() + self.stderr_thread.join() + + + @property + def returncode(self): + """Process return code. Returns None until the process has terminated, + and the stdout/stderr consumer threads have finished. + """ + if self.popen.returncode is None: return None + if self.stdout_thread.is_alive(): return None + if self.stderr_thread.is_alive(): return None + return self.popen.returncode + + + @staticmethod + def check_output(cmd, *args, **kwargs): + """Behaves like subprocess.check_output. Runs the given command, then + waits until it finishes, and return its standard output. An error + is raised if the process returns a non-zero exit code, unless a keyword + argument `check=False` is specified. + + :arg cmd: The command to run, as a string + """ + + check = kwargs.pop('check', True) + proc = Process(cmd, *args, **kwargs) + + proc.wait() + + if check and (proc.returncode != 0): + raise RuntimeError('This command returned an error: ' + cmd) + + stdout = '' + while True: + try: + stdout += proc.stdoutq.get_nowait() + except queue.Empty: + break + + return stdout + + + @staticmethod + def check_call(cmd, *args, **kwargs): + """Behaves like subprocess.check_call. Runs the given command, then + waits until it finishes. An error is raised if the process returns a + non-zero exit code, unless a keyword argument `check=False` is + specified. + + :arg cmd: The command to run, as a string + """ + + check = kwargs.pop('check', True) + proc = Process(cmd, *args, **kwargs) + + proc.wait() + + if check and proc.returncode != 0: + raise RuntimeError('This command returned an error: ' + cmd) + + return proc.returncode + + + @staticmethod + def monitor_progress(cmd, total=None, *args, **kwargs): + """Runs the given command(s), and shows a progress bar under the + assumption that cmd will produce "total" number of lines of output. + + :arg cmd: The commmand to run as a string, or a sequence of + multiple commands. + + :arg total: Total number of lines of standard output to expect. + + :arg timeout: Refresh rate in seconds. Must be passed as a keyword + argument. + + :arg progfunc: Function which returns a number indicating how far + the process has progressed. If provided, this + function is called, instead of standard output + lines being monitored. The function is passed a + reference to the Process object. Must be passed as a + keyword argument. + + :arg progfile: File to write progress updates to. + + :arg proglabel: Label to use when writing progress updates to progfile. + """ + + timeout = kwargs.pop('timeout', 0.5) + progfunc = kwargs.pop('progfunc', None) + proglabel = kwargs.pop('proglabel', None) + progfile = kwargs.pop('progfile', None) + + if total is None: label = None + else: label = '%' + + if progfunc is None: + nlines = [0] + def progfunc(proc): + try: + _ = proc.stdoutq.get_nowait() + nlines[0] = nlines[0] + 1 + except queue.Empty: + pass + return nlines[0] + + if isstr(cmd): cmds = [cmd] + else: cmds = cmd + + with Progress(label=label, + fmt='{:.0f}', + transform=Progress.percent, + proglabel=proglabel, + progfile=progfile) as prog: + + progcount = 0 if total else None + + for cmd in cmds: + + proc = Process(cmd, *args, **kwargs) + prog.update(progcount, total) + + while proc.returncode is None: + time.sleep(timeout) + progcount = progfunc(proc) if total else None + prog.update(progcount, total) + proc.popen.poll() + + # force progress bar to 100% when finished + if proc.returncode == 0: + prog.update(total, total) + else: + raise RuntimeError('This command returned ' + 'an error: ' + cmd) + + + @staticmethod + def forward_stream(stream, + queue, + cmd, + streamname, + log_output, + print_output): + """Reads lines from stream and pushes them onto queue until popen + is finished. Logs every line. + + :arg stream: stream to forward + :arg queue: queue.Queue to push lines onto + :arg cmd: string - the command that is running + :arg streamname: string - 'stdout' or 'stderr' + :arg log_output: If True, log all stdout/stderr. + :arg print_output: If True, print all stdout/stderr. + """ + + while True: + line = stream.readline().decode('utf-8') + if line == '': + break + queue.put(line) + if log_output: + log.debug(' [%s]: %s', streamname, line.rstrip()) + if print_output: + print(' [{}]: {}'.format(streamname, line.rstrip())) + + + @staticmethod + def popen(cmd, admin=False, password=None, append_env=None, **kwargs): + """Runs the given command via subprocess.Popen, as administrator if + requested. + + :arg cmd: The command to run, as a string + + :arg admin: Whether to run with administrative privileges + + :arg pssword: Administrator password. Only required if admin is + True. + + :arg append_env: Dictionary of additional environment to be set when + the command is run. + + :arg kwargs: Passed to subprocess.Popen. stdin, stdout, and stderr + will be silently clobbered + + :returns: The subprocess.Popen object. + """ + + admin = admin and os.getuid() != 0 + + cmd = shlex.split(cmd) + kwargs['stdin'] = sp.PIPE + kwargs['stdout'] = sp.PIPE + kwargs['stderr'] = sp.PIPE + + if admin: + proc = Process.sudo_popen(cmd, password, append_env, **kwargs) + else: + # if append_env has been specified, + # add it to the normal env option. + if append_env is not None: + env = kwargs.get('env', os.environ.copy()) + env.update(append_env) + kwargs['env'] = env + + proc = sp.Popen(cmd, **kwargs) + + return proc + + + @staticmethod + def sudo_popen(cmd, password, append_env=None, **kwargs): + """Runs "sudo cmd" using subprocess.Popen. Used by Process.popen. + Assumes that kwargs contains stdin=sp.PIPE + """ + + # sudo will not necessarily propagate environment + # variables, and there is no guarantee that the + # sudo -E option will work. So here we create a + # wrapper shell script with "export VAR=VALUE" + # statements for all environment variables that + # are set. + if append_env is None: + append_env = {} + + # Make the wrapper script delete itself + # after the command has been executed. + with tempfilename(0o755, delete=False) as wrapper: + with open(wrapper, 'wt') as f: + f.write('#!/usr/bin/env sh\n') + f.write('set -e\n') + f.write('thisfile=$0\n') + f.write('thisdir=$(cd $(dirname $0) && pwd)\n') + for k, v in append_env.items(): + f.write('export {}="{}"\n'.format(k, v)) + # shlex.join not available in py27 + f.write(' '.join(cmd) + '\n') + f.write('cd ${thisdir} && rm ${thisfile}\n') + + cmd = ['sudo', '-S', '-k', wrapper] + proc = sp.Popen(cmd, **kwargs) + proc.stdin.write('{}\n'.format(password).encode()) + proc.stdin.flush() + return proc + + +@ft.total_ordering +class Version(object): + """Class to represent and compare version strings. Accepted version + strings are of the form W.X.Y.Z, where W, X, Y, and Z are all integers. + """ + def __init__(self, verstr): + # Version identifiers for official FSL + # releases will have up to four + # components (X.Y.Z.W), but we accept + # any number of (integer) components, + # as internal releases may have more. + components = [] + + # ignore a leading "v", e.g. v1.2.3 + verstr = verstr.lower() + if verstr.startswith('v'): + verstr = verstr[1:] + + for comp in verstr.split('.'): + try: components.append(int(comp)) + except Exception: break + + self.components = components + self.verstr = verstr + + def __str__(self): + return self.verstr + + def __eq__(self, other): + for sn, on in zip(self.components, other.components): + if sn != on: + return False + return len(self.components) == len(other.components) + + def __lt__(self, other): + for p1, p2 in zip(self.components, other.components): + if p1 < p2: return True + if p1 > p2: return False + return len(self.components) < len(other.components) + + +class Context(object): + """Bag of information and settings created in the main function, and passed + around this script. + + Several settings are lazily evaluated on first access, but once evaluated, + their values are immutable. + """ + + def __init__(self, args, destdir=None, action='install FSL'): + """Create the context with the argparse.Namespace object containing + parsed command-line arguments. + + :arg args: argparse.Namespace containing command-line arguments + :arg destdir: Installation directory. If not provided, read from + args.dest, or read from the user, + :arg action: Passed to get_admin_password as a prompt. + """ + + if destdir is not None: + destdir = op.abspath(destdir) + + self.args = args + self.shell = op.basename(os.environ.get('SHELL', 'sh')).lower() + + # These attributes are updated on-demand via + # the property accessors defined below, or are + # all updated via the finalise_settings method. + self.__platform = None + self.__manifest = None + self.__devmanifest = None + self.__candidate_builds = None + self.__build = None + self.__destdir = destdir + self.__need_admin = None + self.__admin_password = None + self.__action = action + + # If the destination directory already exists, + # and the user chooses to overwrite it, it is + # moved so that, if the installation fails, it + # can be restored. The new path is stored + # here - refer to overwrite_destdir. + self.old_destdir = None + + # The download_fsl_environment function stores + # the path to the FSL conda environment file + # and list of conda channels + self.environment_file = None + self.environment_channels = None + + # The config_logging function stores the path + # to the fslinstaller log file here. + self.logfile = None + + + def finalise_settings(self): + """Finalise values for all information and settings in the Context. + """ + self.manifest + self.candidate_builds + self.platform + self.build + self.destdir + self.need_admin + self.admin_password + + + @property + def license_url(self): + """Return the FSL license URL from the manifest, or None if it is not + present. + """ + return self.manifest['installer'].get('license_url') + + + @property + def registration_url(self): + """Return the FSL registration URL from the manifest, or None if it is + not present. + """ + return self.manifest['installer'].get('registration_url') + + + @property + def platform(self): + """The platform we are running on, e.g. "linux-64", "macos-64", + "macos-M1". This identifier is used to determine which FSL build to + install. + + Note that this function may report a different platform identifier than + the actual platform - specifically, if running on a M1 mac, and there + is no M1 FSL build for the requested FSL version, this function will + report "macos-64". This is because some older FSL releases do not have + M1 builds available. + """ + if self.__platform is None: + plat = identify_platform() + + # if M1, check that we have a suitable + # FSL build, falling back to x86 if not. + if plat == 'macos-M1': + candidates = self.candidate_builds + if not any(c['platform'] == 'macos-M1' for c in candidates): + plat = 'macos-64' + + self.__platform = plat + + return self.__platform + + + @property + def candidate_builds(self): + """Query the manifest and return a list of available builds for the + requested FSL release, for all platforms. + """ + if self.__candidate_builds is not None: + return self.__candidate_builds + + # defaults to "latest" if + # not specified by the user + fslversion = self.args.fslversion + if fslversion is None: + fslversion = 'latest' + + if fslversion not in self.manifest['versions']: + available = ', '.join(self.manifest['versions'].keys()) + raise Exception( + 'FSL version "{}" is not available - available ' + 'versions: {}'.format(fslversion, available)) + + if fslversion == 'latest': + fslversion = self.manifest['versions']['latest'] + + self.__candidate_builds = list(self.manifest['versions'][fslversion]) + + return self.__candidate_builds + + + @property + def build(self): + """Returns a suitable FSL build (a dictionary entry from the FSL + installer manifest) for the target platform. + + The returned dictionary has the following elements: + - 'version' FSL version. + - 'platform': Platform identifier (e.g. 'linux-64') + - 'environment': Environment file to download + - 'sha256': Checksum of environment file + - 'output': Number of lines of expected output, for reporting + progress + """ + + if self.__build is not None: + return self.__build + + # Find refs to a suitable build for this + # platform. We assume that there is only + # one default build for each platform. + # in the list of builds for a given FSL + # version. + candidates = self.candidate_builds + build = None + + for candidate in candidates: + if candidate['platform'] == self.platform: + build = candidate + break + else: + raise Exception( + 'Cannot find a version of FSL matching ' + 'platform {}'.format(self.platform)) + + printmsg('FSL {} selected for installation'.format(build['version'])) + + self.__build = build + return build + + + @property + def destdir(self): + """Installation directory. If not specified at the command line, the + user is prompted to enter a directory. + """ + + if self.__destdir is not None: + return self.__destdir + + fsldir = os.environ.get('FSLDIR', None) + + if fsldir is not None: defdestdir = fsldir + elif os.getuid() != 0: defdestdir = DEFAULT_INSTALLATION_DIRECTORY + else: defdestdir = DEFAULT_ROOT_INSTALLATION_DIRECTORY + + # The loop below validates the destination directory + # both when specified at commmand line or + # interactively. In either case, if invalid, the + # user is re-prompted to enter a new destination. + destdir = None + if self.args.dest is not None: response = self.args.dest + else: response = None + + while destdir is None: + + if response is None: + printmsg('\nWhere do you want to install FSL?', + IMPORTANT, EMPHASIS) + printmsg('Press enter to install to the default location ' + '[{}]\n'.format(defdestdir), INFO) + response = prompt('FSL installation directory [{}]:'.format( + defdestdir), QUESTION, EMPHASIS) + response = response.rstrip(op.sep) + + if response == '': + response = defdestdir + + response = op.expanduser(op.expandvars(response)) + response = op.abspath(response) + parentdir = op.dirname(response) + if op.exists(parentdir): + destdir = response + else: + printmsg('Destination directory {} does not ' + 'exist!'.format(parentdir), ERROR) + response = None + + self.__destdir = destdir + return self.__destdir + + + @property + def conda(self): + """Return a path to the ``conda`` or ``mamba`` executable. """ + bindir = op.join(self.basedir, 'bin') + condabin = op.join(bindir, 'conda') + mambabin = op.join(bindir, 'mamba') + + # If mamba is present, prefer it over conda, unless + # the user requestd otherwise via the --conda flag + if not self.args.conda: candidates = [mambabin, condabin] + else: candidates = [condabin, mambabin] + + for c in candidates: + if op.exists(c): + return c + + raise RuntimeError('Cannot find conda/mamba ' + 'executable in {}'.format(bindir)) + + + @property + def basedir(self): + """Return the path to the base conda installation. For normal + installations this is equivalent to destdir / $FSLDIR, but may be + different if the fslinstaller was instructed to use an existing + [mini]conda installation. + """ + + # Either the user gave a path to an existing + # miniconda installation, or $FSLDIR is the + # base miniconda installation + if (self.args.miniconda is not None) and op.isdir(self.args.miniconda): + return self.args.miniconda + else: + return self.destdir + + + @property + def use_existing_base(self): + """Return True if we have been instructed to use an existing + [mini]conda installation, as opposed to downloading/installing one. + """ + return ((self.args.miniconda is not None) and + op.isdir(self.args.miniconda)) + + + @property + def need_admin(self): + """Returns True if administrator privileges will be needed to install + FSL. + """ + if self.__need_admin is not None: + return self.__need_admin + parentdir = op.dirname(self.destdir) + self.__need_admin = check_need_admin(parentdir) + return self.__need_admin + + + @property + def admin_password(self): + """Returns the user's administrator password, prompting them if needed. + """ + # need_admin may be None or False, + # so don't rely on truthiness. + if not self.need_admin: + return None + if self.__admin_password is None: + self.__admin_password = get_admin_password(self.__action) + return self.__admin_password + + + @property + def manifest(self): + """Returns the FSL installer manifest as a dictionary. """ + + if self.__manifest is None: + if self.devmanifest is not None: + self.args.manifest = self.devmanifest + + self.__manifest = download_manifest( + self.args.manifest, + self.args.workdir, + ssl_verify=(not self.args.skip_ssl_verify)) + return self.__manifest + + + @property + def devmanifest(self): + """Returns a URL to a development manifest to use for installation. + This will only return a value if the --devrelease or --devlatest + options are active. + + If this is the case, the FSL_DEV_RELEASES file is downloaded - this + file contains a list of available development manifest URLS. The + user is then prompted to choose which development manifest to use + for the installation, unless --devlatest is active, in which case + the newest manifest is selected. + """ + if not self.args.devrelease: + return None + if self.__devmanifest == 'na': + return None + elif self.__devmanifest is not None: + return self.__devmanifest + + devreleases = download_dev_releases( + FSL_DEV_RELEASES, + self.args.workdir, + ssl_verify=(not self.args.skip_ssl_verify)) + + if len(devreleases) == 0: + self.__devmanifest = 'na' + return None + + self.__devmanifest = prompt_dev_release(devreleases, + self.args.devlatest) + + return self.__devmanifest + + + def run(self, process_func, *args, **kwargs): + """Run a command via a static Process method. Handles sudo/ + administrator authentication, and ensures that the shell + environment in which the command is executed is sanitised. + + Can be used with Process.check_call, Process.check_output, and + Process.monitor_progress. For example: + + ctx = Context(...) + ctx.run(Process.check_call, 'my_command') + ctx.run(Process.monitor_progress, 'my_command', total=100) + """ + + env = kwargs.pop('env', {}) + append_env = kwargs.pop('append_env', {}) + process_func = ft.partial(process_func, *args, **kwargs) + + # Clear any environment variables that refer to + # existing FSL or conda installations, and ensure + # that some specific FSL environment variables + # are set while the command is running. See + # clean_environ and install_environ for more + # details, and see Process.sudo_popen regarding + # append_env. + env.update(clean_environ()) + append_env.update(install_environ(self.destdir, + self.args.username, + self.args.password)) + return process_func(admin=self.need_admin, + password=self.admin_password, + env=env, + append_env=append_env) + + +def agree_to_license(ctx): + """Prompts the user to agree to the terms of the FSL license.""" + + msg = ['Installing FSL implies agreement with the terms of the FSL ' + 'license - if you do not agree with these terms, you can ' + 'cancel the installation by pressing CTRL+C.', IMPORTANT] + + if ctx.license_url is not None: + msg = msg + [' You can view the license at ', IMPORTANT, + ctx.license_url, IMPORTANT, UNDERLINE] + printmsg(*msg) + printmsg('') + + +def check_rosetta_status(ctx): + """Called from the main routine, before installation is attempted. If + running on a M1 macos machine, and a x86 version of FSL has been selected + for installation, checks whether rosetta emulation is enabled. If so, + does nothing further. Otherwise, prints a message and exits. + """ + + if not all((identify_platform() == 'macos-M1', + ctx.platform == 'macos-64')): + return + + # Using the strategy discussed at + # https://forum.latenightsw.com/t/possible-for-a-script-\ + # to-test-whether-rosetta-2-is-installed/3207/6 + # + # The pkgutil command should return 0 if + # rosetta is installed, non-0 otherwise. + try: + Process.check_output('pkgutil --files com.apple.pkg.RosettaUpdateAuto') + except RuntimeError: + printmsg('Rosetta emulation does not appear to be enabled!\n', ERROR) + printmsg('Enable rosetta emulation, and then run this installer ' + 'again. You can enable rosetta emulation by running this ' + 'command:\n', INFO) + printmsg(' /usr/sbin/softwareupdate --install-rosetta ' + '--agree-to-license\n', IMPORTANT) + printmsg('Aborting installation', ERROR) + sys.exit(1) + # pkgutil command not found - should + # never happen, but print a warning + # just in case + except Exception as e: + printmsg('An error occurred calling the pkgutil command - this ' + 'may not be a problem, so I\'ll attempt to proceed ' + 'with the installation. ({}'.format(e), WARNING) + + +def list_available_versions(manifest): + """Lists available FSL versions. """ + printmsg('Available FSL versions:', EMPHASIS) + for version in manifest['versions']: + if version == 'latest': + continue + printmsg(version, IMPORTANT, EMPHASIS) + for build in manifest['versions'][version]: + printmsg(' {}'.format(build['platform']), EMPHASIS, end=' ') + printmsg(build['environment'], INFO) + + +def prompt_dev_release(devreleases, latest): + """Prompts the user to select a development release. + + :arg devreleases: List of development releases, as returned by + download_dev_releases. + :arg latest: If True, the latest develeopment release is returned. + """ + + if len(devreleases) == 0: + return None + + # automatically choose latest dev manifest? + if latest: + return devreleases[0][0] + + # show the user a list, ask them which one they want + printmsg('Available development releases:', EMPHASIS) + for i, (url, tag, commit, branch) in enumerate(devreleases): + # dev release + if commit is not None: + printmsg(' [{}]: {} [{} commit {}]'.format( + i + 1, tag, branch, commit), IMPORTANT) + # public release + else: + printmsg(' [{}]: {}'.format(i + 1, tag), IMPORTANT) + + while True: + selection = prompt('Which release would you like to ' + 'install? [1]:', PROMPT) + if selection == '': + selection = '1' + try: + selection = int(selection) - 1 + except Exception: + continue + if selection >= 0 and selection < len(devreleases): + break + return devreleases[selection][0] + + +def download_fsl_environment(ctx): + """Downloads the environment specification file for the selected FSL + version. + + Internal/development FSL versions may source packages from the internal + FSL conda channel, which requires a username+password to authenticate. + + These are referred to in the environment file as ${FSLCONDA_USERNAME} + and ${FSLCONDA_PASSWORD}. + + If the user has not provided a username+password on the command-line, they + are prompted for them. + + The downloaded environment file may be modified - if the (hidden) + --exclude_package option has been used. + """ + + build = ctx.build + url = build['environment'] + checksum = build.get('sha256', None) + + printmsg('Downloading FSL environment specification ' + 'from {}...'.format(url)) + fname = url.split('/')[-1] + download_file(url, fname, ssl_verify=(not ctx.args.skip_ssl_verify)) + ctx.environment_file = op.abspath(fname) + if (checksum is not None) and (not ctx.args.no_checksum): + sha256(fname, checksum) + + # Environment files for internal/dev FSL versions + # will list the internal FSL conda channel with + # ${FSLCONDA_USERNAME} and ${FSLCONDA_PASSWORD} + # as placeholders for the username/password. + with open(fname, 'rt') as f: + need_auth = '${FSLCONDA_USERNAME}' in f.read() + + # We need a username/password to access the internal + # FSL conda channel. Prompt the user if they haven't + # provided credentials. + if need_auth and (ctx.args.username is None): + printmsg('A username and password are required to install ' + 'this version of FSL.', WARNING, EMPHASIS) + ctx.args.username = prompt('Username:').strip() + ctx.args.password = getpass.getpass('Password: ').strip() + + # Conda expands environment variables within a + # .condarc file, but *not* within an environment.yml + # file. So to authenticate to our internal channel + # without storing credentials anywhere in plain text, + # we *move* the channel list from the environment.yml + # file into $FSLDIR/.condarc. + # + # Here we extract the channels from the environment + # file, and save them to ctx.environment_channels. + # The install_miniconda function will then add the + # channels to $FSLDIR/.condarc. + # + # We also remove any packages that the user has + # requested to exclude from the installation. + copy = '.' + op.basename(ctx.environment_file) + channels = [] + + shutil.move(ctx.environment_file, copy) + with open(copy, 'rt') as inf, \ + open(ctx.environment_file, 'wt') as outf: + + in_channels_section = False + + for line in inf: + + # start of channels list + if line.strip() == 'channels:': + in_channels_section = True + continue + + if in_channels_section: + # end of channels list + if not line.strip().startswith('-'): + in_channels_section = False + else: + channels.append(line.split()[-1]) + continue + + # Include/exclude packages upon user request + pkgname = line.strip(' -').split()[0] + exclude = match_any(pkgname, ctx.args.exclude_package) + if exclude: + log.debug('Excluding package %s (matched ' + '--exclude_package %s)', line, exclude) + else: + outf.write(line) + + ctx.environment_channels = channels + + +def download_miniconda(ctx): + """Downloads the miniconda/miniforge installer and saves it as + "miniconda.sh". + + This function assumes that it is run within a temporary/scratch directory. + """ + + # The user has specified a path to an + # existing miniconda installation - we + # use that rather than downloading/ + # installing a separate one. + if ctx.use_existing_base: + return + + # user specified a URL/path to a + # miniconda installer + elif ctx.args.miniconda is not None: + url = ctx.args.miniconda + checksum = None + + # Use miniconda installer specified in + # FSL release manifest + else: + metadata = ctx.manifest['miniconda'][ctx.platform] + url = metadata['url'] + checksum = metadata['sha256'] + + # Download + printmsg('Downloading miniconda from {}...'.format(url)) + with Progress('MB', transform=Progress.bytes_to_mb, + proglabel='download_miniconda', + progfile=ctx.args.progress_file) as prog: + download_file(url, 'miniconda.sh', prog.update, + ssl_verify=(not ctx.args.skip_ssl_verify)) + if (not ctx.args.no_checksum) and (checksum is not None): + sha256('miniconda.sh', checksum) + + +def install_miniconda(ctx): + """Downloads the miniconda/miniforge installer, and installs it to the + destination directory. + + This function assumes that it is run within a temporary/scratch directory. + """ + + # We have been instructed to use an + # existing miniconda installation + if ctx.use_existing_base: + return + + metadata = ctx.manifest['miniconda'][ctx.platform] + output = metadata.get('output', '').strip() + + if output == '': output = None + else: output = int(output) + + # The download_miniconda function saved + # the installer to <pwd>/miniconda.sh + printmsg('Installing miniconda at {}...'.format(ctx.basedir)) + cmd = 'bash miniconda.sh -b -p {}'.format(ctx.basedir) + ctx.run(Process.monitor_progress, cmd, total=output, + proglabel='install_miniconda', + progfile=ctx.args.progress_file) + + # Avoid WSL filesystem issue + # https://github.com/conda/conda/issues/9948 + cmd = 'find {} -type f -exec touch {{}} +'.format(ctx.basedir) + ctx.run(Process.check_call, cmd) + + +def generate_condarc(fsldir, + channels, + skip_ssl_verify=False, + pkgsdir=None): + """Called by install_miniconda. Generates content for a .condarc file to + be saved in $FSLDIR/.condarc. This file contains some default values, and + also enforces some settings so that they cannot be overridden by the + user. For example. the list of conda channels is configured so that it + cannot be overridden by a user's ~/.condarc file. + + See the following web pages for more details: + - https://docs.conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html + - https://www.anaconda.com/blog/conda-configuration-engine-power-users + - https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-channels.html + """ + + # Create .condarc config file + condarc = tw.dedent(""" + # FSL conda configuration file, auto-generated by the fslinstaller script. + # + # WARNING: This file may be automatically re-generated + # without warning, so it is recommended that any custom + # conda settings are stored elsewhere. Refer to the conda + # documentation for more guidance: + # + # https://conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html + # https://www.anaconda.com/blog/conda-configuration-engine-power-users + # https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-channels.html + + # Try and make package downloads more robust + remote_read_timeout_secs: 240 + remote_connect_timeout_secs: 20 + remote_max_retries: 10 + remote_backoff_factor: 5 + safety_checks: warn + + # Disable caching of remote channel repodata. + # This is a hack which is combined with the + # WSL1 filesystem hack in the fslinstaller + # script - because we have modified file + # timestamps, conda will assume that its + # channel repodata cache is up to date, and + # will not bother refreshing it in the + # commands that we run in the install_fsl + # function. When we remove the WSL1 hack from + # the fslinstaller, we can remove this config + # setting. + # https://github.com/conda/conda/issues/9948 + local_repodata_ttl: 0 + + # Channel priority is important. In older versions + # of FSL we placed the FSL conda channel at the + # bottom (lowest priority) for legacy reasons (to + # ensure that conda-forge versions of e.g. VTK were + # preferred over legacy FSL conda versions). + # + # Use final/top/bottom marks to prevent the channel + # priority order being modified by user ~/.condarc + # configuration files. + channel_priority: strict #!final + """) + + # Fix the conda package cache + # if a pkgsdir was provided + if pkgsdir is not None: + condarc += tw.dedent(""" + # Fix the package cache at $FSLDIR/pkgs/ + pkgs_dirs: #!final + - {} #!top #!bottom + """.format(pkgsdir)) + + if skip_ssl_verify: + printmsg('Configuring conda to skip SSL verification ' + '- this is not recommended!', WARNING) + condarc += tw.dedent(""" + # Disable SSL verification when accessing + # conda channels over https:// (the + # fslinstaller script was called with + # --skip_ssl_verify). NOT RECOMMENDED. + ssl_verify: false + """) + + channels = list(channels) + if len(channels) > 0: + channels[0] += ' #!top' + channels[-1] += ' #!bottom' + condarc += '\nchannels: #!final\n' + for channel in channels: + condarc += ' - {}\n'.format(channel) + + return condarc + + +def get_install_fsl_progress_reporting_method(ctx): + """Figure out which reporting mechansim to use for reporting progress + whilst FSL is being installed. The mechanism that is used has changed + a few times. + + Returns a tuple containing values to pass to the Progress.monitor_progress + function, either of which may be None if progress cannot be reported: + + - an integer value to pass as the total + - a function to pass as the progfunc. + """ + + # We calculate installation progress in + # one of a few ways, as we have changed + # the mechanism a few times. The + # 'output/install' field in the manifest + # gives us information about how to + # report installation progress. + progparams = ctx.build.get('output', {}).get('install', None) + + # The first method (version 1) involves + # progress reporting by monitoring number of + # lines of standard output produced by + # "conda env update". This is set to None, + # as it is the default behaviour of the + # Progress.monitor_progress function. + progress_v1 = None + + # The remaining methods involve counting + # files and sizes in $FSLDIR/pkgs/ + + # The second method involves progress + # reporting by monitoring the number of + # package files created in $FSLDIR/pkgs/ + # This coarsely reflects download + # progress - when conda downloads a + # package, it is saved into this directory. + # + # The third method involves progress + # reporting by monitoring the number of + # files created in $FSLDIR/pkgs/, + # $FSLDIR/bin/ and $FSLDIR/lib/. Tracking + # these three directories will cause the + # progress to reflect both download and + # installation + # + # The fourth method monitors download + # progress in a more fine-grained manner, + # by calculating the size of all .conda + # and .tar.bz2 files in $FSLDIR/pkgs/. + # This is combined with the number of + # files saved to $FSLDIR/bin/ and + # $FSLDIR/lib/ + pkgdir = op.join(ctx.basedir, 'pkgs') + pkgdir = op.join(ctx.basedir, 'pkgs') + bindir = op.join(ctx.destdir, 'bin') + libdir = op.join(ctx.destdir, 'lib') + + def matchany(name, *filters): + return any([fnmatch.fnmatch(name, f) for f in filters]) + + def contents(dirname, *filters): + if not op.exists(dirname): + return [] + contents = os.listdir(dirname) + contents = [f for f in contents if matchany(f, *filters)] + return [op.join(dirname, f) for f in contents] + + start_pkgs = contents(pkgdir, '*.conda', '*.bz2') + start_sizes = sum([op.getsize(p) for p in start_pkgs]) + start_pkgs = len(start_pkgs) + start_bins = len(contents(bindir)) + start_libs = len(contents(libdir)) + + def progress_v234(v, _): + + pkgs = contents(pkgdir, '*.conda', '*.bz2') + bins = contents(bindir) + libs = contents(libdir) + sizes = [op.getsize(p) for p in pkgs] + pkgs = len(pkgs) - start_pkgs + bins = len(bins) - start_bins + libs = len(libs) - start_libs + sizes = sum(sizes) - start_sizes + + if v == 2: return pkgs + elif v == 3: return pkgs + bins + libs + elif v == 4: return sizes + bins + libs + else: return None + + progresses = {} + progresses[1] = progress_v1 + progresses[2] = ft.partial(progress_v234, 2) + progresses[3] = ft.partial(progress_v234, 3) + progresses[4] = ft.partial(progress_v234, 4) + + progval = None + progfunc = None + + # The output field may be either a + # string, in which case we assume + # version 2, or a dictionary containing + # the progress reporting version, and + # an integer value. + if isstr(progparams): + progval = int(progparams) + progfunc = progresses[2] + + # output field is a dict - versioned + # progress reporting + elif isinstance(progparams, dict): + progver = int(progparams['version']) + progfunc = progresses[progver] + progval = progparams['value'] + + # unsupported progress reporting version + if progver > 4: + progval = None + progfunc = None + + # version 4: progval is a dict + # containing various quantities + if progver == 4: + progval = sum(progval.values()) + + # older versions: progval is an integer + elif progver: + progval = int(progval) + + return progval, progfunc + + +def install_fsl(ctx): + """Install FSL into ctx.destdir (which is assumed to be a miniconda + installation. + + This function assumes that it is run within a temporary/scratch directory. + """ + + progval, progfunc = get_install_fsl_progress_reporting_method(ctx) + + # Generate .condarc which contains some default/ + # fixed conda settings. We create $FSLDIR in + # advance, and copy .condarc into it. Conda seems + # to be ok with the directory already existing, + # although I am concerned that this behaviour may + # not be guaranteed. + # + # If this is a typical FSL installation (a self- + # contained base miniconda environment) we fix + # the package cache directory to isolate it from + # other conda installations that may be on the + # system. + if ctx.destdir == ctx.basedir: pkgsdir = op.join(ctx.destdir, 'pkgs') + else: pkgsdir = None + + condarc_contents = generate_condarc(ctx.destdir, + ctx.environment_channels, + ctx.args.skip_ssl_verify, + pkgsdir) + with open('.condarc', 'wt') as f: + f.write(condarc_contents) + + cmds = ['mkdir -p {}'.format(ctx.destdir), + 'cp -f .condarc {}'.format(ctx.destdir)] + for cmd in cmds: + ctx.run(Process.check_call, cmd) + + # Are we updating an existing + # env or creating a new env? + if ctx.destdir == ctx.basedir: cmd = 'update' + else: cmd = 'create' + + # We install FSL simply by running conda + # env [update|create] -f env.yml. + cmd = (ctx.conda + ' env ' + cmd + + ' -p ' + ctx.destdir + + ' -f ' + ctx.environment_file) + + # Make conda/mamba super verbose if the + # hidden --debug option was specified. + if ctx.args.debug: + cmd += ' -v -v -v' + + printmsg('Installing FSL into {}...'.format(ctx.destdir)) + ctx.run(Process.monitor_progress, cmd, + timeout=2, total=progval, progfunc=progfunc, + proglabel='install_fsl', progfile=ctx.args.progress_file) + + +@warn_on_error('WARNING: The installation succeeded, but an error occurred ' + 'while creating $FSLDIR/etc/fslversion! There may be more ' + 'information in the log file.', WARNING, EMPHASIS) +def finalise_installation(ctx): + """Performs some finalisation tasks. Includes: + - Saving the installed version to $FSLDIR/etc/fslversion + - Saving this installer script and the environment file to + $FSLDIR/etc/ + """ + + with open('fslversion', 'wt') as f: + f.write(ctx.build['version']) + + etcdir = op.join(ctx.destdir, 'etc') + cmds = [ + 'cp fslversion {}' .format(etcdir), + 'cp {} {}' .format(ctx.environment_file, etcdir)] + + for cmd in cmds: + ctx.run(Process.check_call, cmd) + + +@warn_on_error('WARNING: The installation succeeded, but an error occurred ' + 'while removing intermediate package files! There may be more ' + 'information in the log file.', WARNING, EMPHASIS) +def post_install_cleanup(ctx, tmpdir): + """Cleans up the FSL directory after installation. """ + + cmds = [ctx.conda + ' clean -y --all'] + + if tmpdir is not None: + cmds.append('rm -rf ' + tmpdir) + + for cmd in cmds: + ctx.run(Process.check_call, cmd) + + +@warn_on_error('WARNING: ', WARNING, EMPHASIS, toscreen=False) +def register_installation(ctx): + """Gathers and sends some basic system details to the FSL registration + website. + """ + + if ctx.args.skip_registration: + return + + regurl = ctx.registration_url + if regurl is None: + return + + system = platform.system().lower() + uname = Process.check_output('uname -a', check=False) + osinfo = '' + + # macOS + if system == 'darwin': + osinfo = Process.check_output('sw_vers', check=False) + + # Linux + else: + for releasefile in glob.glob(op.join('/etc/*-release')): + with open(releasefile, 'rt') as f: + osinfo = f.read().strip() + break + + # WSL + if 'microsoft' in uname.lower(): + osinfo += '\n\n' + Process.check_output('wsl.exe -v', check=False) + + info = { + 'timestamp' : timestamp(), + 'architecture' : platform.machine(), + 'os' : system, + 'os_info' : osinfo, + 'uname' : uname, + 'python_version' : platform.python_version(), + 'python_info' : sys.version, + 'fsl_version' : ctx.build['version'], + 'fsl_platform' : ctx.build['platform'], + } + + printmsg('Registering installation with {}'.format(regurl)) + + post_request(regurl, data=info) + + +def patch_file(filename, searchline, numlines, content): + """Used by configure_shell and configure_matlab. Adds to, modifies, + or creates the specified file. + + If a line matching searchline is found in the file, numlines (starting + from searchline) are replaced with content. + + Otherwise, content is appended to the end of the file. + """ + + content = content.split('\n') + + if op.isfile(filename): + with open(filename) as f: + lines = [l.strip() for l in f.readlines()] + else: + lines = [] + + # replace block + try: + idx = lines.index(searchline) + lines = lines[:idx] + content + lines[idx + numlines:] + + # append to end + except ValueError: + lines = lines + [''] + content + [''] + + with open(filename, 'wt') as f: + f.write('\n'.join(lines)) + + +def configure_shell(shell, homedir, fsldir): + """Configures the user's shell environment (e.g. ~/.bash_profile). + + :arg shell: User's shell (taken from the $SHELL environment variable + :arg homedir: User's home directory, presumed to contain shell profile + file(s). + :arg fsldir: FSL installation directory + """ + + bourne_shells = ['sh', 'bash', 'zsh', 'dash'] + csh_shells = ['csh', 'tcsh'] + + # we edit the first file that exists in + # the list of candidate profile files. + # They are attached as an attribute of + # this function just for testing purposes + # (see after function definition) + shell_profiles = configure_shell.shell_profiles + + # DO NOT CHANGE the format of these configurations - + # they are kept exactly as-is for compatibility with + # legacy FSL installations, i.e. so we can modify + # profiles with an existing configuration from older + # FSL versions + bourne_cfg = tw.dedent(""" + # FSL Setup + FSLDIR={fsldir} + PATH=${{FSLDIR}}/share/fsl/bin:${{PATH}} + export FSLDIR PATH + . ${{FSLDIR}}/etc/fslconf/fsl.sh + """).format(fsldir=fsldir).strip() + + csh_cfg = tw.dedent(""" + # FSL Setup + setenv FSLDIR {fsldir} + setenv PATH ${{FSLDIR}}/share/fsl/bin:${{PATH}} + source ${{FSLDIR}}/etc/fslconf/fsl.csh + """).format(fsldir=fsldir).strip() + + if shell not in bourne_shells + csh_shells: + printmsg('Shell {} not recognised - skipping environment ' + 'setup'.format(shell), WARNING, EMPHASIS) + return + + if shell in bourne_shells: cfg = bourne_cfg + else: cfg = csh_cfg + + # find the profile file to edit + profile = None + candidates = [op.join(homedir, p) + for p in shell_profiles[shell]] + for candidate in candidates: + if op.isfile(candidate): + profile = candidate + break + + # if no candidate profile files + # exist, fall back to the first one + if profile is None: + profile = candidates[0] + + printmsg('Adding FSL configuration to {}'.format(profile)) + + patch_file(profile, '# FSL Setup', len(cfg.split('\n')), cfg) +configure_shell.shell_profiles = {'sh' : ['.profile'], + 'ksh' : ['.profile'], + 'bash' : ['.bash_profile', '.profile'], + 'dash' : ['.bash_profile', '.profile'], + 'zsh' : ['.zprofile'], + 'csh' : ['.cshrc'], + 'tcsh' : ['.tcshrc']} + + +def configure_matlab(homedir, fsldir): + """Creates/appends FSL configuration code to ~/Documents/MATLAB/startup.m. + """ + + # DO NOT CHANGE the format of this configuration - + # see in-line comments in configure_shell. + cfg = tw.dedent(""" + % FSL Setup + setenv( 'FSLDIR', '{fsldir}' ); + setenv('FSLOUTPUTTYPE', 'NIFTI_GZ'); + fsldir = getenv('FSLDIR'); + fsldirmpath = sprintf('%s/etc/matlab',fsldir); + path(path, fsldirmpath); + clear fsldir fsldirmpath; + """).format(fsldir=fsldir).strip() + + matlab_dir = op.expanduser(op.join(homedir, 'Documents', 'MATLAB')) + startup_m = op.join(matlab_dir, 'startup.m') + + if not op.exists(matlab_dir): + os.makedirs(matlab_dir) + + printmsg('Adding FSL configuration to {}'.format(startup_m)) + + patch_file(startup_m, '% FSL Setup', len(cfg.split('\n')), cfg) + + +def self_update(manifest, workdir, checksum, **kwargs): + """Checks to see if a newer version of the installer (this script) is + available and if so, downloads it to a temporary file, and runs it in + place of this script. + """ + + thisver = Version(__version__) + latestver = Version(manifest['installer']['version']) + + if latestver <= thisver: + log.debug('Installer is up to date (this version: %s, ' + 'latest version: %s)', thisver, latestver) + return + + log.debug('New version of installer is available ' + '(%s) - self-updating', latestver) + + tmpf = tempfile.NamedTemporaryFile( + prefix='new_fslinstaller', delete=False, dir=workdir) + tmpf.close() + tmpf = tmpf.name + + download_file(manifest['installer']['url'], tmpf, **kwargs) + + if checksum: + try: + sha256(tmpf, manifest['installer']['sha256']) + except Exception as e: + printmsg('New installer file does not match expected ' + 'checksum! Skipping update.', WARNING) + return + + # Don't try and update again - if for some + # reason the online manifest reports a newer + # version than what is available, we would + # otherwise enter into an infinite loop. + cmd = [sys.executable, tmpf] + sys.argv[1:] + ['--no_self_update'] + log.debug('Running new installer: %s', cmd) + os.execv(sys.executable, cmd) + + +def overwrite_destdir(ctx): + """Called by main to handle when the destination directory already exists. + Asks the user if they want to overwrite it. If they do, or if the + --overwrite option was specified, the directory is moved, and then deleted + after the installation succeeds. + + This function assumes that it is run within a temporary/scratch directory. + """ + + # there is no existing installation, + # so there is nothing to worry about + if not op.exists(ctx.destdir): + return + + # We have been instructed to install + # into an existing [mini]conda environment + if ctx.use_existing_base and (ctx.basedir == ctx.destdir): + return + + if not ctx.args.overwrite: + printmsg('\nDestination directory [{}] already exists!\n' + .format(ctx.destdir), WARNING, EMPHASIS) + response = prompt('Do you want to overwrite it [y/N]?', + QUESTION, EMPHASIS) + if response.lower() not in ('y', 'yes'): + printmsg('Aborting installation', ERROR, EMPHASIS) + sys.exit(1) + + # generate a unique name for the old + # destination directory (to avoid + # collisions if using the same workdir + # repeatedly) + i = 0 + while True: + ctx.old_destdir = op.abspath('old_destdir{}'.format(i)) + i += 1 + if not op.exists(ctx.old_destdir): + break + + printmsg('Deleting directory {}'.format(ctx.destdir), IMPORTANT) + ctx.run(Process.check_call, + 'mv {} {}'.format(ctx.destdir, ctx.old_destdir)) + + +def parse_args(argv=None, include=None, parser=None): + """Parse command-line arguments, returns an argparse.Namespace object. + + :arg argv: Command-line arguments. + + :arg include: List of arguments to parse. May be used by other scripts + which re-use some of the routines defined in this script. + The resulting argparse.Namespace object will contain values + of None for all arguments that are not included. + + :arg parser: `argparse.ArgumentParser` to configure. If not provided, + one is created. + """ + + if parser is None: + parser = argparse.ArgumentParser() + + uid = os.getuid() + + if uid != 0: destdir = DEFAULT_INSTALLATION_DIRECTORY + else: destdir = DEFAULT_ROOT_INSTALLATION_DIRECTORY + + # on macOS, when Python is run with sudo, + # op.expanduser('~') will return the + # calling user's home directory, and not + # the root home directory. This doesn't + # really matter, as homedir is only used + # for modifying the shell/matlab profile, + # and this is automatically disabled via + # the --no_env option when run as root. But + # in case the user wants the root shell + # profile modified (via the hidden + # --root_env option), we use getpwuid to + # determine the appropriate home directory. + homedir = pwd.getpwuid(uid).pw_dir + + username = os.environ.get('FSLCONDA_USERNAME', None) + password = os.environ.get('FSLCONDA_PASSWORD', None) + + options = { + # regular options + 'version' : ('-v', {'action' : 'version', + 'version' : __version__}), + 'dest' : ('-d', {'metavar' : 'DESTDIR'}), + 'overwrite' : ('-o', {'action' : 'store_true'}), + 'listversions' : ('-l', {'action' : 'store_true'}), + 'no_env' : ('-n', {'action' : 'store_true'}), + 'no_shell' : ('-s', {'action' : 'store_true'}), + 'no_matlab' : ('-m', {'action' : 'store_true'}), + 'skip_registration' : ('-r', {'action' : 'store_true'}), + 'fslversion' : ('-V', {'default' : 'latest'}), + + # hidden options + 'debug' : (None, {'action' : 'store_true'}), + 'logfile' : (None, {}), + 'username' : (None, {'default' : username}), + 'password' : (None, {'default' : password}), + 'no_checksum' : (None, {'action' : 'store_true'}), + 'skip_ssl_verify' : (None, {'action' : 'store_true'}), + 'workdir' : (None, {}), + 'homedir' : (None, {'default' : homedir}), + 'devrelease' : (None, {'action' : 'store_true'}), + 'devlatest' : (None, {'action' : 'store_true'}), + 'manifest' : (None, {}), + 'miniconda' : (None, {}), + 'conda' : (None, {'action' : 'store_true'}), + 'no_self_update' : (None, {'action' : 'store_true'}), + 'exclude_package' : (None, {'action' : 'append'}), + 'root_env' : (None, {'action' : 'store_true'}), + 'progress_file' : (None, {}), + } + + if include is None: + include = list(options.keys()) + + helps = { + 'version' : 'Print installer version number and exit.', + 'listversions' : 'List available FSL versions and exit.', + 'dest' : 'Install FSL into this folder (default: ' + '{}).'.format(destdir), + 'overwrite' : 'Delete existing destination directory if it ' + 'exists, without asking.', + 'no_env' : 'Do not modify your shell or MATLAB configuration ' + '(implies --no_shell and --no_matlab). When ' + 'running the installer script as the root user, ' + 'the root shell profile is never modified.', + 'no_shell' : 'Do not modify your shell configuration.', + 'no_matlab' : 'Do not modify your MATLAB configuration.', + 'skip_registration' : 'Do not register this installation with the ' + 'FSL development team.', + 'fslversion' : 'Install this specific version of FSL.', + + # Enable verbose output when calling + # mamba/conda. + 'debug' : argparse.SUPPRESS, + + # Direct the installer log to this file + # (default: file in $TMPDIR) + 'logfile' : argparse.SUPPRESS, + + # Username / password for accessing + # internal FSL conda channel, if an + # internal/development release is being + # installed. If not set, will be read from + # FSLCONDA_USERNAME/FSLCONDA_PASSWORD + # environment variables. + 'username' : argparse.SUPPRESS, + 'password' : argparse.SUPPRESS, + + # Do not automatically update the installer script, + 'no_self_update' : argparse.SUPPRESS, + + # Install a development release. This + # option will cause the installer to + # download the devrelreases.txt file, + # which contains a list of available + # internal/development manifests. The + # user will be prompted to choose one, + # which will be propagated on to the + # --manifest option. If --devlatest + # is used, the most recent developmet + # release is automatically selected. + 'devrelease' : argparse.SUPPRESS, + 'devlatest' : argparse.SUPPRESS, + + # Path/URL to alternative FSL release + # manifest. + 'manifest' : argparse.SUPPRESS, + + # Install miniconda from this path/URL, + # instead of the one specified in the + # FSL release manifest. + # + # For example - to download/install a + # conda base environment and install FSL + # packages into the base environment, + # pass a URL or path to a miniconda + # installer: + # + # fslinstaller.py --miniconda https://path/to/miniconda.sh + # fslinstaller.py --miniconda ~/Downloads/miniconda.sh + # + # Alternatively, pass the directory of + # an existing [mini]conda installation + # to use that - for example, if a conda + # base environment has already been + # created at ~/fsl/, to install FSL into + # that base environment: + # + # fslinstaller.py --miniconda ~/fsl/ + # + # Or to use an existing [mini]conda + # installation, and create FSL as a + # child environment, just set the + # destination directory to a + # different path, e.g.: + # + # fslinstaller.py --miniconda ~/miniconda3/ -d ~/fsl/ + 'miniconda' : argparse.SUPPRESS, + + # Use conda and not mamba + 'conda' : argparse.SUPPRESS, + + # Disable SHA256 checksum validation + # of downloaded files + 'no_checksum' : argparse.SUPPRESS, + + # Store temp files in this directory + # rather than in a temporary directory + 'workdir' : argparse.SUPPRESS, + + # Treat this directory as user's home + # directory, for the purposes of shell + # configuration. Must already exist. + 'homedir' : argparse.SUPPRESS, + + # Configure conda to skip SSL verification. + # Not recommended. + 'skip_ssl_verify' : argparse.SUPPRESS, + + # Do not install packages matching this + # fnmatch-style wildcard pattern. Can + # be used multiple times. + 'exclude_package' : argparse.SUPPRESS, + + # If the installer is run as root, the + # --no_env flag is automatically enabled + # UNLESS this flag is also provided. + 'root_env' : argparse.SUPPRESS, + + # File to send progress information to. + 'progress_file' : argparse.SUPPRESS, + } + + # parse args + for option in include: + shortflag, kwargs = options[option] + flags = ['--{}'.format(option)] + if shortflag is not None: + flags.insert(0, shortflag) + parser.add_argument(*flags, help=helps[option], **kwargs) + + # parse_known_args so that newly added + # args are ignored by older versions, + # but will be parsed after self_update + args = parser.parse_known_args(argv)[0] + + if getattr(args, 'fslversion', 'latest') != 'latest': + if Version(args.fslversion) < Version('6.0.6'): + printmsg( + 'This script can only be used to install FSL 6.0.6 or newer. ' + 'Visit https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FslInstallation ' + 'for information on installing older versions.', ERROR, + EMPHASIS) + sys.exit(1) + + # add placeholder values for excluded args + for option in options.keys(): + if option not in include: + setattr(args, option, None) + + # alternate home dir (for debugging) + if args.homedir is not None: + args.homedir = op.abspath(args.homedir) + if not op.isdir(args.homedir): + printmsg('Home directory {} does not exist!'.format(args.homedir), + ERROR, EMPHASIS) + sys.exit(1) + + # --no-env is automatically enabled + # when installer is run as root + if os.getuid() == 0 and not (args.root_env): + args.no_env = True + + # don't modify shell profile + if args.no_env: + args.no_shell = True + args.no_matlab = True + + # use workdir rather than a tempdir + if args.workdir is not None: + args.workdir = op.abspath(args.workdir) + if not op.exists(args.workdir): + os.mkdir(args.workdir) + + # manifest takes priority over devrelease/devlatest + if args.manifest is not None: + args.devrelease = False + args.devlatest = False + + if args.manifest is None: + args.manifest = FSL_RELEASE_MANIFEST + + if args.devlatest: + args.devrelease = True + + if args.exclude_package is None: + args.exclude_package = [] + + if args.logfile is not None: + args.logfile = op.abspath(args.logfile) + if args.progress_file is not None: + args.progress_file = op.abspath(args.progress_file) + + # accept local path for manifest and environment + if args.manifest is not None: + args.manifest = op.expanduser(args.manifest) + if op.exists(args.manifest): + args.manifest = op.abspath(args.manifest) + + # accept local path for miniconda installer, or + # path to existing miniconda installation + if args.miniconda is not None: + args.miniconda = op.expanduser(args.miniconda) + if op.exists(args.miniconda): + args.miniconda = op.abspath(args.miniconda) + + return args + + +def config_logging(prefix='fslinstaller_', logdir=None, logfile=None): + """Configures logging. If a logfile is not specified, log messages are + directed to $TMPDIR/fslinstaller_<unique_token>.log, or + logdir/fslinstaller_<unique_token>.log + """ + + if logfile is None: + if logdir is None: + logdir = tempfile.gettempdir() + + # Use a unique name for the log file + # (important for multi-user systems) + logfilef, logfile = tempfile.mkstemp(prefix=prefix, + suffix='.log', + dir=logdir) + os.close(logfilef) + + handler = logging.FileHandler(logfile) + formatter = logging.Formatter( + '%(asctime)s %(filename)s:%(lineno)4d: %(message)s', '%H:%M:%S') + handler.setFormatter(formatter) + log.addHandler(handler) + log.setLevel(logging.DEBUG) + + return logfile + + +@contextlib.contextmanager +def handle_error(ctx): + """Used by main as a context manager around the main installation steps. + If an error occurs, prints some messages, performs some clean-up/ + restoration tasks, and exits. + """ + + try: + yield + + except Exception as e: + printmsg('\nERROR occurred during installation!', ERROR, EMPHASIS) + printmsg(' {}\n'.format(e), INFO) + + # send traceback to log file + tb = traceback.format_tb(sys.exc_info()[2]) + log.debug(''.join(tb)) + + # send env to logfile + log.debug('Environment variables:') + for k, v in os.environ.items(): + log.debug('{}={}'.format(k, v)) + + if op.exists(ctx.destdir): + printmsg('Removing failed installation directory ' + '{}'.format(ctx.destdir), WARNING) + ctx.run(Process.check_call, 'rm -r ' + ctx.destdir) + + # overwrite_destdir moves the existing + # destdir to a temp location, so we can + # restore it if the installation fails + if not op.exists(ctx.destdir) and (ctx.old_destdir is not None): + printmsg('Restoring contents of {}'.format(ctx.destdir), + WARNING) + ctx.run(Process.check_call, + 'mv {} {}'.format(ctx.old_destdir, ctx.destdir)) + + # copy log file to ~/ so it is + # easier for the user to access + date = datetime.datetime.today().strftime('%Y%m%d%H%M%S') + logfile = 'fsl_installation_{}.log'.format(date) + logfile = op.join(op.expanduser('~'), logfile) + shutil.copy(ctx.logfile, logfile) + + printmsg('\nFSL installation failed!\n', ERROR, EMPHASIS) + printmsg('Please check the log file - it may contain some ' + 'more information to help you diagnose the problem: ' + '{}\n'.format(logfile), WARNING, EMPHASIS) + sys.exit(1) + + +def main(argv=None): + """Installer entry point. Downloads and installs miniconda and FSL, and + configures the user's environment. + """ + + printmsg('FSL installer version:', EMPHASIS, UNDERLINE, end='') + printmsg(' {}'.format(__version__)) + printmsg('Press CTRL+C at any time to cancel installation', INFO) + + if os.getuid() == 0: + printmsg('Running the installer script as root user is discouraged! ' + 'You should run this script as a regular user - you will be ' + 'asked for your administrator password if required.', + WARNING, EMPHASIS) + + args = parse_args(argv) + logfile = config_logging(logdir=args.workdir, logfile=args.logfile) + + log.debug(' '.join(sys.argv)) + log.debug('Python: %s', sys.executable) + printmsg('Installation log file: {}\n'.format(logfile), INFO) + + ctx = Context(args) + ctx.logfile = logfile + + if not args.no_self_update: + self_update(ctx.manifest, args.workdir, not args.no_checksum, + ssl_verify=(not args.skip_ssl_verify)) + + if args.listversions: + list_available_versions(ctx.manifest) + sys.exit(0) + + agree_to_license(ctx) + + if (not args.skip_registration) and (ctx.registration_url is not None): + printmsg('During the installation process, please note that some ' + 'system details will be automatically sent to the FSL ' + 'development team. These details are extremely basic and ' + 'cannot be used in any way to identify individual users. If ' + 'you do not want any information to be sent, please cancel ' + 'this installation by pressing CTRL+C, and re-run the ' + 'installer with the --skip_registration option.\n', INFO) + + try: + ctx.finalise_settings() + except Exception as e: + printmsg('An error has occurred: {}'.format(e), ERROR) + sys.exit(1) + + check_rosetta_status(ctx) + + # Do everything in a temporary directory, + # but don't delete it, as some operations + # may be run as root. The tempdir is + # deleted within the post_install_cleanup + # function. + with tempdir(args.workdir, delete=False) as tmpdir: + + if args.workdir is not None: + tmpdir = None + + # Ask the user if they want to overwrite + # an existing installation + overwrite_destdir(ctx) + + download_fsl_environment(ctx) + + printmsg('\nInstalling FSL in {}\n'.format(ctx.destdir), EMPHASIS) + with handle_error(ctx): + download_miniconda(ctx) + install_miniconda(ctx) + install_fsl(ctx) + finalise_installation(ctx) + post_install_cleanup(ctx, tmpdir) + register_installation(ctx) + + if not args.no_shell: + configure_shell(ctx.shell, args.homedir, ctx.destdir) + if not args.no_matlab: + configure_matlab(args.homedir, ctx.destdir) + + printmsg('\nFSL successfully installed\n', IMPORTANT) + if not args.no_shell: + printmsg('Open a new terminal, or log out and log back in, ' + 'for the environment changes to take effect.', INFO) + + +if __name__ == '__main__': + sys.exit(main()) @@ -1,10 +1,33 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- +#!/usr/bin/python +# EASY-INSTALL-ENTRY-SCRIPT: 'fslpy==3.2.2','console_scripts','imcp' import re import sys -from fsl.scripts.imcp import main +# for compatibility with easy_install; see #2198 +__requires__ = 'fslpy==3.2.2' + +try: + from importlib.metadata import distribution +except ImportError: + try: + from importlib_metadata import distribution + except ImportError: + from pkg_resources import load_entry_point + + +def importlib_load_entry_point(spec, group, name): + dist_name, _, _ = spec.partition('==') + matches = ( + entry_point + for entry_point in distribution(dist_name).entry_points + if entry_point.group == group and entry_point.name == name + ) + return next(matches).load() + + +globals().setdefault('load_entry_point', importlib_load_entry_point) + if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) + sys.exit(load_entry_point('fslpy==3.2.2', 'console_scripts', 'imcp')()) @@ -1,10 +1,33 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- +#!/usr/bin/python +# EASY-INSTALL-ENTRY-SCRIPT: 'fslpy==3.2.2','console_scripts','imglob' import re import sys -from fsl.scripts.imglob import main +# for compatibility with easy_install; see #2198 +__requires__ = 'fslpy==3.2.2' + +try: + from importlib.metadata import distribution +except ImportError: + try: + from importlib_metadata import distribution + except ImportError: + from pkg_resources import load_entry_point + + +def importlib_load_entry_point(spec, group, name): + dist_name, _, _ = spec.partition('==') + matches = ( + entry_point + for entry_point in distribution(dist_name).entry_points + if entry_point.group == group and entry_point.name == name + ) + return next(matches).load() + + +globals().setdefault('load_entry_point', importlib_load_entry_point) + if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) + sys.exit(load_entry_point('fslpy==3.2.2', 'console_scripts', 'imglob')()) @@ -1,10 +1,33 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- +#!/usr/bin/python +# EASY-INSTALL-ENTRY-SCRIPT: 'fslpy==3.2.2','console_scripts','immv' import re import sys -from fsl.scripts.immv import main +# for compatibility with easy_install; see #2198 +__requires__ = 'fslpy==3.2.2' + +try: + from importlib.metadata import distribution +except ImportError: + try: + from importlib_metadata import distribution + except ImportError: + from pkg_resources import load_entry_point + + +def importlib_load_entry_point(spec, group, name): + dist_name, _, _ = spec.partition('==') + matches = ( + entry_point + for entry_point in distribution(dist_name).entry_points + if entry_point.group == group and entry_point.name == name + ) + return next(matches).load() + + +globals().setdefault('load_entry_point', importlib_load_entry_point) + if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) + sys.exit(load_entry_point('fslpy==3.2.2', 'console_scripts', 'immv')()) diff --git a/systemvars.mk b/systemvars.mk deleted file mode 100755 index 33d1cc67c8b8..000000000000 --- a/systemvars.mk +++ /dev/null @@ -1,46 +0,0 @@ -# $Id: systemvars.mk,v 1.6 2019/02/14 13:29:45 mwebster Exp $ - -# System dependent paths - -RM = /bin/rm -CHMOD = /bin/chmod -MKDIR = /bin/mkdir -CP = /bin/cp -MV = /bin/mv -INSTALL = install -p -TCLSH = /usr/bin/tclsh -RANLIB = echo - -FSLML = ${FSLDIR}/bin/fslml - -# for SHELL, do not change the type of shell - only use Bourne or BASH -SHELL = /bin/sh - -# Compiler dependent variables - -CC = gcc -CXX = c++ -CXX11 = c++ -CSTATICFLAGS = -static -CXXSTATICFLAGS = -static - -ARCHFLAGS = -m64 -ARCHLDFLAGS = -Wl,-rpath,'$$ORIGIN/../lib' - -PARALLELFLAGS = -fopenmp - -DEPENDFLAGS = -MM - -OPTFLAGS = -march=native -g -O3 -fexpensive-optimizations ${ARCHFLAGS} -MACHDBGFLAGS = -g -GNU_ANSI_FLAGS = -Wall -ansi -pedantic -std=c++11 -Wno-long-long -SGI_ANSI_FLAGS = -ansi -fullwarn -ANSI_FLAGS = ${GNU_ANSI_FLAGS} - -# CUDA development environment -CUDA_INSTALLATION = /opt/cuda -GENCODE_FLAGS = $(shell ${FSLDIR}/config/common/supportedGencodes.sh ${CUDA_INSTALLATION}) -LIB_CUDA = ${CUDA_INSTALLATION}/lib64 -INC_CUDA = ${CUDA_INSTALLATION}/include -NVCC = ${CUDA_INSTALLATION}/bin/nvcc -NVCC11= ${CUDA_INSTALLATION}/bin/nvcc |