Skip to content
Snippets Groups Projects
Commit db453f52 authored by Paul McCarthy's avatar Paul McCarthy :mountain_bicyclist:
Browse files

Merge branch 'enh/wsl' into 'master'

Enh/wsl

See merge request fsl/fslpy!228
parents b4ae9ac1 bdce436f
No related branches found
No related tags found
No related merge requests found
...@@ -6,6 +6,18 @@ order. ...@@ -6,6 +6,18 @@ order.
------------------------- -------------------------
Added
^^^^^
* New :func:`.winpath` and :func:`wslpath` functions for working with paths
when using FSL in a Windows Subsystem for Linux (WSL) environment.
* New :func:`.wslcmd` function for generating a path to a FSL command installed
in a WSL environment.
* New :meth:`.Platform.fslwsl` attribute for detecting whether FSL is installed
in a WSL environment.
Fixed Fixed
^^^^^ ^^^^^
......
...@@ -23,6 +23,8 @@ paths. ...@@ -23,6 +23,8 @@ paths.
removeDuplicates removeDuplicates
uniquePrefix uniquePrefix
commonBase commonBase
wslpath
winpath
""" """
...@@ -30,6 +32,9 @@ import os.path as op ...@@ -30,6 +32,9 @@ import os.path as op
import os import os
import glob import glob
import operator import operator
import re
from fsl.utils.platform import platform
class PathError(Exception): class PathError(Exception):
...@@ -524,3 +529,58 @@ def commonBase(paths): ...@@ -524,3 +529,58 @@ def commonBase(paths):
return base return base
raise PathError('No common base') raise PathError('No common base')
def wslpath(winpath):
"""
Convert Windows path (or a command line argument containing a Windows path)
to the equivalent WSL path (e.g. ``c:\\Users`` -> ``/mnt/c/Users``). Also supports
paths in the form ``\\wsl$\\(distro)\\users\\...``
:param winpath: Command line argument which may (or may not) contain a Windows path. It is assumed to be
either of the form <windows path> or --<arg>=<windows path>. Note that we don't need to
handle --arg <windows path> or -a <windows path> since in these cases the argument
and the path will be parsed as separate entities.
:return: If ``winpath`` matches a Windows path, the converted argument (including the --<arg>= portion).
Otherwise returns ``winpath`` unchanged.
"""
match = re.match(r"^(--[\w-]+=)?\\\\wsl\$[\\\/][^\\^\/]+(.*)$", winpath)
if match:
arg, path = match.group(1, 2)
if arg is None:
arg = ""
return arg + path.replace("\\", "/")
match = re.match(r"^(--[\w-]+=)?([a-zA-z]):(.+)$", winpath)
if match:
arg, drive, path = match.group(1, 2, 3)
if arg is None:
arg = ""
return arg + "/mnt/" + drive.lower() + path.replace("\\", "/")
return winpath
def winpath(wslpath):
"""
Convert a WSL-local filepath (for example ``/usr/local/fsl/``) into a path that can be used from
Windows.
If ``self.fslwsl`` is ``False``, simply returns ``wslpath`` unmodified
Otherwise, uses ``FSLDIR`` to deduce the WSL distro in use for FSL.
This requires WSL2 which supports the ``\\wsl$\`` network path.
wslpath is assumed to be an absolute path.
"""
if not platform.fslwsl:
return wslpath
else:
match = re.match(r"^\\\\wsl\$\\([^\\]+).*$", platform.fsldir)
if match:
distro = match.group(1)
else:
distro = None
if not distro:
raise RuntimeError("Could not identify WSL installation from FSLDIR (%s)" % platform.fsldir)
return "\\\\wsl$\\" + distro + wslpath.replace("/", "\\")
...@@ -292,6 +292,12 @@ class Platform(notifier.Notifier): ...@@ -292,6 +292,12 @@ class Platform(notifier.Notifier):
return os.environ.get('FSLDEVDIR', None) return os.environ.get('FSLDEVDIR', None)
@property
def fslwsl(self):
"""Boolean flag indicating whether FSL is installed in Windows Subsystem for Linux """
return self.fsldir is not None and self.fsldir.startswith("\\\\wsl$")
@fsldir.setter @fsldir.setter
def fsldir(self, value): def fsldir(self, value):
"""Changes the value of the :attr:`fsldir` property, and notifies any """Changes the value of the :attr:`fsldir` property, and notifies any
......
...@@ -20,21 +20,22 @@ ...@@ -20,21 +20,22 @@
""" """
import sys import sys
import shlex import shlex
import logging import logging
import threading import threading
import contextlib import contextlib
import collections import collections.abc as abc
import subprocess as sp import subprocess as sp
import os.path as op import os.path as op
import os
import six
import six
from fsl.utils.platform import platform as fslplatform from fsl.utils.platform import platform as fslplatform
import fsl.utils.fslsub as fslsub import fsl.utils.fslsub as fslsub
import fsl.utils.tempdir as tempdir import fsl.utils.tempdir as tempdir
import fsl.utils.path as fslpath
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -202,7 +203,7 @@ def run(*args, **kwargs): ...@@ -202,7 +203,7 @@ def run(*args, **kwargs):
if submit is True: if submit is True:
submit = dict() submit = dict()
if submit is not None and not isinstance(submit, collections.Mapping): if submit is not None and not isinstance(submit, abc.Mapping):
raise ValueError('submit must be a mapping containing ' raise ValueError('submit must be a mapping containing '
'options for fsl.utils.fslsub.submit') 'options for fsl.utils.fslsub.submit')
...@@ -359,7 +360,12 @@ def runfsl(*args, **kwargs): ...@@ -359,7 +360,12 @@ def runfsl(*args, **kwargs):
args = prepareArgs(args) args = prepareArgs(args)
for prefix in prefixes: for prefix in prefixes:
cmdpath = op.join(prefix, args[0]) cmdpath = op.join(prefix, args[0])
if op.isfile(cmdpath): if fslplatform.fslwsl:
wslargs = wslcmd(cmdpath, *args)
if wslargs is not None:
args = wslargs
break
elif op.isfile(cmdpath):
args[0] = cmdpath args[0] = cmdpath
break break
...@@ -372,6 +378,53 @@ def runfsl(*args, **kwargs): ...@@ -372,6 +378,53 @@ def runfsl(*args, **kwargs):
return run(*args, **kwargs) return run(*args, **kwargs)
def wslcmd(cmdpath, *args):
"""
Convert a command + arguments into an equivalent set of arguments that will run the command
under Windows Subsystem for Linux
:param cmdpath: Fully qualified path to the command. This is essentially a WSL path not a Windows
one since FSLDIR is specified as a WSL path, however it may have backslashes
as path separators due to previous use of ``os.path.join``
:param args: Sequence of command arguments (the first of which is the unqualified command name)
:return: If ``cmdpath`` exists and is executable in WSL, return a sequence of command arguments
which when executed will run the command in WSL. Windows paths in the argument list will
be converted to WSL paths. If ``cmdpath`` was not executable in WSL, returns None
"""
# Check if command exists in WSL (remembering that the command path may include FSLDIR which
# is a Windows path)
cmdpath = fslpath.wslpath(cmdpath)
retcode = sp.call(["wsl", "test", "-x", cmdpath])
if retcode == 0:
# Form a new argument list and convert any Windows paths in it into WSL paths
wslargs = [fslpath.wslpath(arg) for arg in args]
wslargs[0] = cmdpath
local_fsldir = fslpath.wslpath(fslplatform.fsldir)
if fslplatform.fsldevdir:
local_fsldevdir = fslpath.wslpath(fslplatform.fsldevdir)
else:
local_fsldevdir = None
# Prepend important environment variables - note that it seems we cannot
# use WSLENV for this due to its insistance on path mapping. FIXME FSLDEVDIR?
local_path = "$PATH"
if local_fsldevdir:
local_path += ":%s/bin" % local_fsldevdir
local_path += ":%s/bin" % local_fsldir
prepargs = [
"wsl",
"PATH=%s" % local_path,
"FSLDIR=%s" % local_fsldir,
"FSLOUTPUTTYPE=%s" % os.environ.get("FSLOUTPUTTYPE", "NIFTI_GZ")
]
if local_fsldevdir:
prepargs.append("FSLDEVDIR=%s" % local_fsldevdir)
return prepargs + wslargs
else:
# Command was not found in WSL with this path
return None
def wait(job_ids): def wait(job_ids):
"""Proxy for :func:`.fslsub.wait`. """ """Proxy for :func:`.fslsub.wait`. """
return fslsub.wait(job_ids) return fslsub.wait(job_ids)
...@@ -13,6 +13,7 @@ import shutil ...@@ -13,6 +13,7 @@ import shutil
import tempfile import tempfile
import pytest import pytest
import mock
import fsl.utils.path as fslpath import fsl.utils.path as fslpath
import fsl.data.image as fslimage import fsl.data.image as fslimage
...@@ -1395,3 +1396,19 @@ def test_commonBase(): ...@@ -1395,3 +1396,19 @@ def test_commonBase():
for ft in failtests: for ft in failtests:
with pytest.raises(fslpath.PathError): with pytest.raises(fslpath.PathError):
fslpath.commonBase(ft) fslpath.commonBase(ft)
def test_wslpath():
assert fslpath.wslpath('c:\\Users\\Fishcake\\image.nii.gz') == '/mnt/c/Users/Fishcake/image.nii.gz'
assert fslpath.wslpath('--input=x:\\transfers\\scratch\\image_2.nii') == '--input=/mnt/x/transfers/scratch/image_2.nii'
assert fslpath.wslpath('\\\\wsl$\\centos 7\\users\\fsl\\file.nii') == '/users/fsl/file.nii'
assert fslpath.wslpath('--file=\\\\wsl$\\centos 7\\home\\fsl\\img.nii.gz') == '--file=/home/fsl/img.nii.gz'
assert fslpath.wslpath('\\\\wsl$/centos 7/users\\fsl\\file.nii') == '/users/fsl/file.nii'
def test_winpath():
"""
See comment for ``test_fslwsl`` for why we are overwriting FSLDIR
"""
with mock.patch.dict('os.environ', **{ 'FSLDIR' : '\\\\wsl$\\my cool linux distro v2.0\\usr\\local\\fsl'}):
assert fslpath.winpath("/home/fsl/myfile.dat") == '\\\\wsl$\\my cool linux distro v2.0\\home\\fsl\\myfile.dat'
with mock.patch.dict('os.environ', **{ 'FSLDIR' : '/opt/fsl'}):
assert fslpath.winpath("/home/fsl/myfile.dat") == '/home/fsl/myfile.dat'
...@@ -212,3 +212,17 @@ def test_detect_ssh(): ...@@ -212,3 +212,17 @@ def test_detect_ssh():
p = fslplatform.Platform() p = fslplatform.Platform()
assert not p.inSSHSession assert not p.inSSHSession
assert not p.inVNCSession assert not p.inVNCSession
def test_fslwsl():
"""
Note that ``Platform.fsldir`` requires the directory in ``FSLDIR`` to exist and
sets ``FSLDIR`` to ``None`` if it doesn't. So we create a ``Platform`` first
and then overwrite ``FSLDIR``. This is a bit of a hack but the logic we are testing
here is whether ``Platform.fslwsl`` recognizes a WSL ``FSLDIR`` string
"""
p = fslplatform.Platform()
with mock.patch.dict('os.environ', **{ 'FSLDIR' : '\\\\wsl$\\my cool linux distro v1.0\\usr\\local\\fsl'}):
assert p.fslwsl
with mock.patch.dict('os.environ', **{ 'FSLDIR' : '/usr/local/fsl'}):
assert not p.fslwsl
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment