Skip to content
Snippets Groups Projects
Commit 3a874cf2 authored by Martin Craig's avatar Martin Craig Committed by Paul McCarthy
Browse files

Implement revised version of WSL interface using \\wsl$\ mapping

This requires  WSL 2.0 but enables FSLDIR to be a genuine
WIndows path so that atlases and standard data can be loaded
transparently. It also means we no longer need FSLWSL environment
variable, just to be a bit careful with path mapping
parent c68b8755
No related branches found
No related tags found
No related merge requests found
...@@ -17,6 +17,7 @@ import os ...@@ -17,6 +17,7 @@ import os
import os.path as op import os.path as op
import sys import sys
import importlib import importlib
import re
import fsl.utils.notifier as notifier import fsl.utils.notifier as notifier
...@@ -285,7 +286,6 @@ class Platform(notifier.Notifier): ...@@ -285,7 +286,6 @@ class Platform(notifier.Notifier):
""" """
return os.environ.get('FSLDIR', None) return os.environ.get('FSLDIR', None)
@property @property
def fsldevdir(self): def fsldevdir(self):
"""The FSL development directory location. """ """The FSL development directory location. """
...@@ -294,7 +294,7 @@ class Platform(notifier.Notifier): ...@@ -294,7 +294,7 @@ class Platform(notifier.Notifier):
@property @property
def fslwsl(self): def fslwsl(self):
"""Boolean flag indicating whether FSL is installed in Windows Subsystem for Linux """ """Boolean flag indicating whether FSL is installed in Windows Subsystem for Linux """
return os.environ.get('FSLWSL', "0") == "1" return self.fsldir is not None and self.fsldir.startswith("\\\\wsl$\\")
@fsldir.setter @fsldir.setter
def fsldir(self, value): def fsldir(self, value):
...@@ -405,6 +405,58 @@ class Platform(notifier.Notifier): ...@@ -405,6 +405,58 @@ class Platform(notifier.Notifier):
""" """
return self.__glIsSoftware return self.__glIsSoftware
def wslpath(self, 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(self, 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 self.fslwsl:
return wslpath
else:
match = re.match(r"^\\\\wsl\$\\([^\\]+).*$", self.fsldir)
if match:
distro = match.group(1)
else:
distro = None
if distro is None:
raise RuntimeError("No WSL installations found at \\wsl$\ - a valid WSL 2.0 installation is required")
else:
return "\\\\wsl$\\" + distro + wslpath.replace("/", "\\")
platform = Platform() platform = Platform()
"""An instance of the :class:`Platform` class. Feel free to create your own """An instance of the :class:`Platform` class. Feel free to create your own
......
...@@ -29,7 +29,6 @@ import collections ...@@ -29,7 +29,6 @@ import collections
import subprocess as sp import subprocess as sp
import os.path as op import os.path as op
import os import os
import re
import six import six
...@@ -392,44 +391,39 @@ def wslcmd(cmdpath, *args): ...@@ -392,44 +391,39 @@ def wslcmd(cmdpath, *args):
which when executed will run the command in WSL. Windows paths in the argument list will 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 be converted to WSL paths. If ``cmdpath`` was not executable in WSL, returns None
""" """
# Check if command exists in WSL (translating Windows path separators to Unix as os.path.join # Check if command exists in WSL (remembering that the command path may include FSLDIR which
# may have inserted some backslashes) # is a Windows path)
cmdpath = cmdpath.replace("\\", "/") cmdpath = fslplatform.wslpath(cmdpath)
retcode = sp.call(["wsl", "test", "-x", cmdpath]) retcode = sp.call(["wsl", "test", "-x", cmdpath])
if retcode == 0: if retcode == 0:
# Form a new argument list and convert any Windows paths in it into WSL paths # Form a new argument list and convert any Windows paths in it into WSL paths
wslargs = [wslpath(arg) for arg in args] wslargs = [fslplatform.wslpath(arg) for arg in args]
wslargs[0] = cmdpath wslargs[0] = cmdpath
local_fsldir = fslplatform.wslpath(fslplatform.fsldir)
if fslplatform.fsldevdir:
local_fsldevdir = fslplatform.wslpath(fslplatform.fsldevdir)
else:
local_fsldevdir = None
# Prepend important environment variables - note that it seems we cannot # Prepend important environment variables - note that it seems we cannot
# use WSLENV for this due to its insistance on path mapping. # use WSLENV for this due to its insistance on path mapping. FIXME FSLDEVDIR?
return [ local_path = "$PATH"
if local_fsldevdir:
local_path += ":%s/bin" % local_fsldevdir
local_path += ":%s/bin" % local_fsldir
prepargs = [
"wsl", "wsl",
"PATH=$PATH:%s/bin" % fslplatform.fsldir, "PATH=%s" % local_path,
"FSLDIR=%s" % fslplatform.fsldir, "FSLDIR=%s" % local_fsldir,
"FSLOUTPUTTYPE=%s" % os.environ.get("FSLOUTPUTTYPE", "NIFTI_GZ") "FSLOUTPUTTYPE=%s" % os.environ.get("FSLOUTPUTTYPE", "NIFTI_GZ")
] + wslargs ]
if local_fsldevdir:
prepargs.append("FSLDEVDIR=%s" % local_fsldevdir)
return prepargs + wslargs
else: else:
# Command was not found in WSL with this path # Command was not found in WSL with this path
return None return None
def wslpath(patharg):
"""
Convert a command line argument containing a Windows path to the equivalent WSL path (e.g. ``c:\\Users`` -> ``/mnt/c/Users``)
:param patharg: 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>
:return: If ``patharg`` matches a Windows path, the converted argument (including the --<arg>= portion). Otherwise
returns ``patharg`` unchanged.
"""
match = re.match("^(--[\w-]+=)?([a-zA-z]):(.+)$", patharg)
if match:
arg, drive, path = match.group(1, 2, 3)
if arg is None:
arg = ""
return arg + "/mnt/" + drive.lower() + path.replace("\\", "/")
else:
return patharg
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)
...@@ -214,11 +214,32 @@ def test_detect_ssh(): ...@@ -214,11 +214,32 @@ def test_detect_ssh():
assert not p.inVNCSession assert not p.inVNCSession
def test_fslwsl(): def test_fslwsl():
"""
with mock.patch.dict('os.environ', **{ 'FSLWSL' : '1'}): Note that ``Platform.fsldir`` requires the directory in ``FSLDIR`` to exist and
p = fslplatform.Platform() 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 assert p.fslwsl
with mock.patch.dict('os.environ', **{ 'FSLWSL' : '0'}): with mock.patch.dict('os.environ', **{ 'FSLDIR' : '/usr/local/fsl'}):
p = fslplatform.Platform()
assert not p.fslwsl assert not p.fslwsl
def test_wslpath():
p = fslplatform.Platform()
assert p.wslpath('c:\\Users\\Fishcake\\image.nii.gz') == '/mnt/c/Users/Fishcake/image.nii.gz'
assert p.wslpath('--input=x:\\transfers\\scratch\\image_2.nii') == '--input=/mnt/x/transfers/scratch/image_2.nii'
assert p.wslpath('\\\\wsl$\\centos 7\\users\\fsl\\file.nii') == '/users/fsl/file.nii'
assert p.wslpath('--file=\\\\wsl$\\centos 7\\home\\fsl\\img.nii.gz') == '--file=/home/fsl/img.nii.gz'
def test_winpath():
"""
See comment for ``test_fslwsl``
"""
p = fslplatform.Platform()
with mock.patch.dict('os.environ', **{ 'FSLDIR' : '\\\\wsl$\\my cool linux distro v2.0\\usr\\local\\fsl'}):
assert p.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 p.winpath("/home/fsl/myfile.dat") == '/home/fsl/myfile.dat'
...@@ -410,7 +410,3 @@ def test_run_logcmd(): ...@@ -410,7 +410,3 @@ def test_run_logcmd():
assert stdout == expstdout assert stdout == expstdout
assert open('my_stdout', 'rt').read() == expcmd + expstdout assert open('my_stdout', 'rt').read() == expcmd + expstdout
def test_wslpath():
assert run.wslpath('c:\\Users\\Fishcake\\image.nii.gz') == '/mnt/c/Users/Fishcake/image.nii.gz'
assert run.wslpath('--input=x:\\transfers\\scratch\\image_2.nii') == '--input=/mnt/x/transfers/scratch/image_2.nii'
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