Repository: mesonbuild/meson

Issue:
`meson setup` fails on Windows with an absolute path for libdir on E:\ drive and is case sensitive for Windows drive
**Describe the bug**
I'm learning to use Meson following the tutorial (which for Windows requires quite a lot of additional research to get things to work - something to address another time)

I have python and Meson on the C:\ drive, source and libraries on E:\. (E:\ is currently a USB drive if that's relevant.)
When trying to add a dependency to a library, I tried to set the libdir to the library location on E:\ with `meson setup build --libdir=E:/Documents/Coding/C/lib` (after an `rm -rf build`) and got a python stack trace in response:

```
$ meson setup build --libdir=E:/Documents/Coding/C/lib
The Meson build system
Version: 0.52.1
Source dir: E:\Documents\Coding\C\meson_test
Build dir: E:\Documents\Coding\C\meson_test\build
Build type: native build
Traceback (most recent call last):
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\mesonmain.py", line 129, in run
    return options.run_func(options)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\msetup.py", line 245, in run
    app.generate()
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\msetup.py", line 159, in generate
    self._generate(env)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\msetup.py", line 176, in _generate
    intr = interpreter.Interpreter(b)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\interpreter.py", line 2110, in __init__
    self.parse_project()
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\interpreterbase.py", line 397, in parse_project
    self.evaluate_codeblock(self.ast, end=1)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\interpreterbase.py", line 436, in evaluate_codeblock
    raise e
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\interpreterbase.py", line 430, in evaluate_codeblock
    self.evaluate_statement(cur)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\interpreterbase.py", line 441, in evaluate_statement
    return self.function_call(cur)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\interpreterbase.py", line 776, in function_call
    return func(node, posargs, kwargs)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\interpreterbase.py", line 143, in wrapped
    return f(*wrapped_args, **wrapped_kwargs)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\interpreterbase.py", line 174, in wrapped
    return f(*wrapped_args, **wrapped_kwargs)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\interpreter.py", line 2723, in func_project
    self.coredata.set_default_options(default_options, self.subproject, self.environment)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\coredata.py", line 742, in set_default_options
    self.set_options(options, subproject=subproject)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\coredata.py", line 674, in set_options
    if self._try_set_builtin_option(k, v):
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\coredata.py", line 531, in _try_set_builtin_option
    value = self.sanitize_dir_option_value(prefix, optname, value)
  File "C:\Users\<user>\AppData\Roaming\Python\Python38\site-packages\mesonbuild\coredata.py", line 479, in sanitize_dir_option_value
    if os.path.commonpath([value, prefix]) != str(PurePath(prefix)):
  File "c:\users\liz\appdata\local\programs\python\python38\lib\ntpath.py", line 763, in commonpath
    raise ValueError("Paths don't have the same drive")
ValueError: Paths don't have the same drive
```
This is possibly related to #3239

If I try the same instruction but with the libraries copied to the C:\ drive, I get the following response:

```
$ meson setup build --libdir=C:/Documents/Coding/C/lib
The Meson build system
Version: 0.52.1
Source dir: E:\Documents\Coding\C\meson_test
Build dir: E:\Documents\Coding\C\meson_test\build
Build type: native build

meson.build:1:0: ERROR: The value of the 'libdir' option is 'C:/Documents/Coding/C/lib' which must be a subdir of the prefix 'c:/'.
Note that if you pass a relative path, it is assumed to be a subdir of prefix.

A full log can be found at E:\Documents\Coding\C\meson_test\build\meson-logs\meson-log.txt
```

If I do the same again but with `$ meson setup build --libdir=c:/Documents/Coding/C/lib` (note the lowercase `c`) I get the same response as for `$ meson setup build` (i.e. it seems to work).

(I'm aware that I should be setting the prefix argument to the "installation directory" but I'm not yet sure what that is)

**Files To Reproduce**
I don't think the contents of the files matters. These are as barebones as it gets.
[main.c](https://github.com/mesonbuild/meson/files/4010104/main.c.txt)
[meson.build](https://github.com/mesonbuild/meson/files/4010136/meson.build.txt)

**Expected behavior**
I would expect Meson to show me an error message and a way to fix things (rather than a stack trace).
I would also expect Windows paths to be case insensitive or to be warned that they are case sensitive.

**System Parameters**
* Native build
* Windows 10
* Python 3.8.1
* `meson --version` => `0.52.1`
* `ninja --version` => `1.9.0.git.kitware.dyndep-1.jobserver-1`

**Notes**
Thanks for all your work on Meson, it looks it will be a useful tool once I've figured it out.
If I can provide any more information, do let me know.


Discussion:

Resolving patch:
diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py
--- a/mesonbuild/coredata.py
+++ b/mesonbuild/coredata.py
@@ -466,7 +466,7 @@ def sanitize_prefix(self, prefix):
                 prefix = prefix[:-1]
         return prefix
 
-    def sanitize_dir_option_value(self, prefix, option, value):
+    def sanitize_dir_option_value(self, prefix: str, option: str, value: Any) -> Any:
         '''
         If the option is an installation directory option and the value is an
         absolute path, check that it resides within prefix and return the value
@@ -474,22 +474,31 @@ def sanitize_dir_option_value(self, prefix, option, value):
 
         This way everyone can do f.ex, get_option('libdir') and be sure to get
         the library directory relative to prefix.
+
+        .as_posix() keeps the posix-like file seperators Meson uses.
         '''
-        if option.endswith('dir') and os.path.isabs(value) and \
+        try:
+            value = PurePath(value)
+        except TypeError:
+            return value
+        if option.endswith('dir') and value.is_absolute() and \
            option not in builtin_dir_noprefix_options:
             # Value must be a subdir of the prefix
             # commonpath will always return a path in the native format, so we
             # must use pathlib.PurePath to do the same conversion before
             # comparing.
-            if os.path.commonpath([value, prefix]) != str(PurePath(prefix)):
-                m = 'The value of the {!r} option is {!r} which must be a ' \
-                    'subdir of the prefix {!r}.\nNote that if you pass a ' \
-                    'relative path, it is assumed to be a subdir of prefix.'
-                raise MesonException(m.format(option, value, prefix))
-            # Convert path to be relative to prefix
-            skip = len(prefix) + 1
-            value = value[skip:]
-        return value
+            msg = ('The value of the {!r} option is {!r} which must be a '
+                   'subdir of the prefix {!r}.\nNote that if you pass a '
+                   'relative path, it is assumed to be a subdir of prefix.')
+            # os.path.commonpath doesn't understand case-insensitive filesystems,
+            # but PurePath().relative_to() does.
+            try:
+                value = value.relative_to(prefix)
+            except ValueError:
+                raise MesonException(msg.format(option, value, prefix))
+            if '..' in str(value):
+                raise MesonException(msg.format(option, value, prefix))
+        return value.as_posix()
 
     def init_builtins(self):
         # Create builtin options with default values
diff --git a/run_unittests.py b/run_unittests.py
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -1750,7 +1750,8 @@ def test_absolute_prefix_libdir(self):
         https://github.com/mesonbuild/meson/issues/1345
         '''
         testdir = os.path.join(self.common_test_dir, '90 default options')
-        prefix = '/someabs'
+        # on Windows, /someabs is *not* an absolute path
+        prefix = 'x:/someabs' if is_windows() else '/someabs'
         libdir = 'libdir'
         extra_args = ['--prefix=' + prefix,
                       # This can just be a relative path, but we want to test
@@ -1771,16 +1772,25 @@ def test_libdir_must_be_inside_prefix(self):
         '''
         testdir = os.path.join(self.common_test_dir, '1 trivial')
         # libdir being inside prefix is ok
-        args = ['--prefix', '/opt', '--libdir', '/opt/lib32']
+        if is_windows():
+            args = ['--prefix', 'x:/opt', '--libdir', 'x:/opt/lib32']
+        else:
+            args = ['--prefix', '/opt', '--libdir', '/opt/lib32']
         self.init(testdir, extra_args=args)
         self.wipe()
         # libdir not being inside prefix is not ok
-        args = ['--prefix', '/usr', '--libdir', '/opt/lib32']
+        if is_windows():
+            args = ['--prefix', 'x:/usr', '--libdir', 'x:/opt/lib32']
+        else:
+            args = ['--prefix', '/usr', '--libdir', '/opt/lib32']
         self.assertRaises(subprocess.CalledProcessError, self.init, testdir, extra_args=args)
         self.wipe()
         # libdir must be inside prefix even when set via mesonconf
         self.init(testdir)
-        self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=/opt', False)
+        if is_windows():
+            self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=x:/opt', False)
+        else:
+            self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=/opt', False)
 
     def test_prefix_dependent_defaults(self):
         '''
@@ -7155,14 +7165,11 @@ def main():
         import pytest # noqa: F401
         # Need pytest-xdist for `-n` arg
         import xdist # noqa: F401
-        if sys.version_info.major <= 3 and sys.version_info.minor <= 5:
-            raise ImportError('pytest with python <= 3.5 is causing issues on the CI')
         pytest_args = ['-n', 'auto', './run_unittests.py']
         pytest_args += convert_args(sys.argv[1:])
         return subprocess.run(python_command + ['-m', 'pytest'] + pytest_args).returncode
     except ImportError:
         print('pytest-xdist not found, using unittest instead')
-        pass
     # All attempts at locating pytest failed, fall back to plain unittest.
     cases = ['InternalTests', 'DataTests', 'AllPlatformTests', 'FailureTests',
              'PythonTests', 'NativeFileTests', 'RewriterTests', 'CrossFileTests',


Test patch:
diff --git a/run_project_tests.py b/run_project_tests.py
--- a/run_project_tests.py
+++ b/run_project_tests.py
@@ -380,7 +380,7 @@ def _run_test(testdir, test_build_dir, install_dir, extra_args, compiler, backen
     setup_env = None
     # Configure in-process
     if pass_prefix_to_test(testdir):
-        gen_args = ['--prefix', '/usr']
+        gen_args = ['--prefix', 'x:/usr'] if mesonlib.is_windows() else ['--prefix', '/usr']
     else:
         gen_args = []
     if pass_libdir_to_test(testdir):
@@ -545,6 +545,10 @@ def skippable(suite, test):
     if not suite.endswith('frameworks'):
         return True
 
+    # this test assumptions aren't valid for Windows paths
+    if test.endswith('38 libdir must be inside prefix'):
+        return True
+
     # gtk-doc test may be skipped, pending upstream fixes for spaces in
     # filenames landing in the distro used for CI
     if test.endswith('10 gtk-doc'):
diff --git a/test cases/common/105 testframework options/meson.build b/test cases/common/105 testframework options/meson.build
--- a/test cases/common/105 testframework options/meson.build	
+++ b/test cases/common/105 testframework options/meson.build	
@@ -1,3 +1,6 @@
+# normally run only from run_tests.py or run_project_tests.py
+# else do like
+# meson build '-Dtestoption=A string with spaces' -Dother_one=true -Dcombo_opt=one -Dprefix=/usr -Dlibdir=lib -Dbackend=ninja -Dwerror=True
 project('options', 'c')
 
 assert(get_option('testoption') == 'A string with spaces', 'Incorrect value for testoption option.')
diff --git a/test cases/failing/38 libdir must be inside prefix/meson.build b/test cases/failing/38 libdir must be inside prefix/meson.build
--- a/test cases/failing/38 libdir must be inside prefix/meson.build	
+++ b/test cases/failing/38 libdir must be inside prefix/meson.build	
@@ -1,2 +1,6 @@
 project('libdir prefix', 'c',
   default_options : ['libdir=/opt/lib'])
+
+if host_machine.system() == 'windows'
+  error('MESON_SKIP_TEST: this test does not work on Windows since /foo is not absolute')
+endif
\ No newline at end of file