Commit db453f52 authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

Merge branch 'enh/wsl' into 'master'

Enh/wsl

See merge request fsl/fslpy!228
parents b4ae9ac1 bdce436f
......@@ -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
^^^^^
......
......@@ -23,6 +23,8 @@ paths.
removeDuplicates
uniquePrefix
commonBase
wslpath
winpath
"""
......@@ -30,6 +32,9 @@ import os.path as op
import os
import glob
import operator
import re
from fsl.utils.platform import platform
class PathError(Exception):
......@@ -524,3 +529,58 @@ def commonBase(paths):
return 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):
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
def fsldir(self, value):
"""Changes the value of the :attr:`fsldir` property, and notifies any
......
......@@ -20,21 +20,22 @@
"""
import sys
import shlex
import logging
import threading
import contextlib
import collections
import subprocess as sp
import os.path as op
import six
import sys
import shlex
import logging
import threading
import contextlib
import collections.abc as abc
import subprocess as sp
import os.path as op
import os
import six
from fsl.utils.platform import platform as fslplatform
import fsl.utils.fslsub as fslsub
import fsl.utils.tempdir as tempdir
import fsl.utils.path as fslpath
log = logging.getLogger(__name__)
......@@ -202,7 +203,7 @@ def run(*args, **kwargs):
if submit is True:
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 '
'options for fsl.utils.fslsub.submit')
......@@ -359,7 +360,12 @@ def runfsl(*args, **kwargs):
args = prepareArgs(args)
for prefix in prefixes:
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
break
......@@ -372,6 +378,53 @@ def runfsl(*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):
"""Proxy for :func:`.fslsub.wait`. """
return fslsub.wait(job_ids)
......@@ -13,6 +13,7 @@ import shutil
import tempfile
import pytest
import mock
import fsl.utils.path as fslpath
import fsl.data.image as fslimage
......@@ -1395,3 +1396,19 @@ def test_commonBase():
for ft in failtests:
with pytest.raises(fslpath.PathError):
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():
p = fslplatform.Platform()
assert not p.inSSHSession
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
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment