Repository: mesonbuild/meson

Issue:
Build directory is allowed to be parent directory of project
**Describe the bug**
It is possible, for any project containing meson.build, to configure the parent directory as the build directory. This is easy to do by accident (if you create a build subdirectory and forget to change into it). Critically, 'meson setup --wipe' may then erase the project itself, and any other sibling project.

**To Reproduce**
`meson setup ..`

**Expected behavior**
This should be rejected with an error message. The build directory should not be allowed to be the parent directory of the project containing the `meson.build` file.

Moreover, there seems to be general confusion about the semantics of the directory argument to `meson setup <dir>` since it seems the following are accepted:

`<dir>` is the build directory, current working directory is the project (i.e. contains meson.build)
`<dir>` is the project directory, current working directory is the build directory

While I can understand the occasional convenience of allowing the 2nd form, it doesn't seem to be documented and the fact that it works leads to confusion. The whole bug would be avoided if the 2nd form was rejected, as the documentation implies it should be.

What makes the bug even worse is that `meson setup --wipe ..` will happily erase your project directory, and anything else that happens to be in the same parent directory. It's pretty easy to accidentally configure the parent as build dir i.e.:

1. `meson setup ..` -  whoops, forgot to change into the build subdir first, parent is now configured as build dir. Let's suppose I don't realise, but do see that I wanted a different configuration. Or even that I do realise, but forget to remove the various stuff that meson created in the parent directory so it remains configured as a build directory, then some time later make a similar mistake of forgetting to change into build directory before I do:
2. `meson setup .. -wipe` - whoops, the whole project is gone and so is any other project that happened to be in the parent directory.

I just got hit by this bug. I'm pretty sure I haven't lost anything important, and I realise I'm somewhat to blame, but the fact that this can happen so easily is not good.

**system parameters**
* Is this a [cross build](https://mesonbuild.com/Cross-compilation.html) or just a plain native build (for the same computer)?
Native build, but probably it doesn't matter.
* what operating system (e.g. MacOS Catalina, Windows 10, CentOS 8.0, Ubuntu 18.04, etc.)
Linux.
* what Python version are you using e.g. 3.8.0
~~3.7.5~~ 3.11.1
* what `meson --version`
1.0.0
* what `ninja --version` if it's a Ninja build
N/A, not necessary to run ninja to reproduce.

Make sure that the source directory never lies inside the build direc…
…tory.

Fixes #11418

Note: This PR is a duplicate of #11427


Discussion:

Resolving patch:
diff --git a/mesonbuild/msetup.py b/mesonbuild/msetup.py
--- a/mesonbuild/msetup.py
+++ b/mesonbuild/msetup.py
@@ -24,6 +24,7 @@
 import tempfile
 import shutil
 import glob
+from pathlib import Path
 
 from . import environment, interpreter, mesonlib
 from . import build
@@ -157,6 +158,8 @@ def add_vcs_ignore_files(self, build_dir: str) -> None:
 
     def validate_dirs(self, dir1: str, dir2: str, reconfigure: bool, wipe: bool) -> T.Tuple[str, str]:
         (src_dir, build_dir) = self.validate_core_dirs(dir1, dir2)
+        if Path(build_dir) in Path(src_dir).parents:
+            raise MesonException(f'Build directory {build_dir} cannot be a parent of source directory {src_dir}')
         if not os.listdir(build_dir):
             self.add_vcs_ignore_files(build_dir)
             return src_dir, build_dir
@@ -165,16 +168,21 @@ def validate_dirs(self, dir1: str, dir2: str, reconfigure: bool, wipe: bool) ->
         has_partial_build = os.path.isdir(priv_dir)
         if has_valid_build:
             if not reconfigure and not wipe:
-                print('Directory already configured.\n'
-                      '\nJust run your build command (e.g. ninja) and Meson will regenerate as necessary.\n'
+                print('Directory already configured.\n\n'
+                      'Just run your build command (e.g. ninja) and Meson will regenerate as necessary.\n'
                       'If ninja fails, run "ninja reconfigure" or "meson setup --reconfigure"\n'
-                      'to force Meson to regenerate.\n'
-                      '\nIf build failures persist, run "meson setup --wipe" to rebuild from scratch\n'
-                      'using the same options as passed when configuring the build.'
-                      '\nTo change option values, run "meson configure" instead.')
-                raise SystemExit
-        elif not has_partial_build and 'MESON_RUNNING_IN_PROJECT_TESTS' not in os.environ:
-            raise SystemExit(f'Directory is not empty and does not contain a previous build:\n{build_dir}')
+                      'to force Meson to regenerate.\n\n'
+                      'If build failures persist, run "meson setup --wipe" to rebuild from scratch\n'
+                      'using the same options as passed when configuring the build.\n'
+                      'To change option values, run "meson configure" instead.')
+                # FIXME: This returns success and ignores new option values from CLI.
+                # We should either make this a hard error, or update options and
+                # return success.
+                # Note that making this an error would not be backward compatible (and also isn't
+                # universally agreed on): https://github.com/mesonbuild/meson/pull/4249.
+                raise SystemExit(0)
+        elif not has_partial_build and wipe:
+            raise MesonException(f'Directory is not empty and does not contain a previous build:\n{build_dir}')
         return src_dir, build_dir
 
     def generate(self) -> None:
diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py
--- a/unittests/allplatformstests.py
+++ b/unittests/allplatformstests.py
@@ -1139,7 +1139,7 @@ def test_always_prefer_c_compiler_for_asm(self):
 
     def test_preprocessor_checks_CPPFLAGS(self):
         '''
-        Test that preprocessor compiler checks read CPPFLAGS and also CFLAGS but
+        Test that preprocessor compiler checks read CPPFLAGS and also CFLAGS/CXXFLAGS but
         not LDFLAGS.
         '''
         testdir = os.path.join(self.common_test_dir, '132 get define')
@@ -1150,11 +1150,13 @@ def test_preprocessor_checks_CPPFLAGS(self):
         # % and # confuse the MSVC preprocessor
         # !, ^, *, and < confuse lcc preprocessor
         value = 'spaces and fun@$&()-=_+{}[]:;>?,./~`'
-        for env_var in ['CPPFLAGS', 'CFLAGS']:
+        for env_var in [{'CPPFLAGS'}, {'CFLAGS', 'CXXFLAGS'}]:
             env = {}
-            env[env_var] = f'-D{define}="{value}"'
+            for i in env_var:
+                env[i] = f'-D{define}="{value}"'
             env['LDFLAGS'] = '-DMESON_FAIL_VALUE=cflags-read'
             self.init(testdir, extra_args=[f'-D{define}={value}'], override_envvars=env)
+            self.new_builddir()
 
     def test_custom_target_exe_data_deterministic(self):
         testdir = os.path.join(self.common_test_dir, '109 custom target capture')
@@ -2044,6 +2046,7 @@ def test_subproject_promotion(self):
         self.assertFalse(os.path.isfile(promoted_wrap))
         subprocess.check_call(self.wrap_command + ['promote', 'athing'], cwd=workdir)
         self.assertTrue(os.path.isfile(promoted_wrap))
+        self.new_builddir()  # Ensure builddir is not parent or workdir
         self.init(workdir)
         self.build()
 
diff --git a/unittests/platformagnostictests.py b/unittests/platformagnostictests.py
--- a/unittests/platformagnostictests.py
+++ b/unittests/platformagnostictests.py
@@ -163,3 +163,38 @@ def test_change_backend(self):
 
         # Wipe with a different backend is allowed
         self.init(testdir, extra_args=['--wipe', '--backend=none'])
+
+    def test_validate_dirs(self):
+        testdir = os.path.join(self.common_test_dir, '1 trivial')
+
+        # Using parent as builddir should fail
+        self.builddir = os.path.dirname(self.builddir)
+        with self.assertRaises(subprocess.CalledProcessError) as cm:
+            self.init(testdir)
+        self.assertIn('cannot be a parent of source directory', cm.exception.stdout)
+
+        # Reconfigure of empty builddir should work
+        self.new_builddir()
+        self.init(testdir, extra_args=['--reconfigure'])
+
+        # Reconfigure of not empty builddir should work
+        self.new_builddir()
+        Path(self.builddir, 'dummy').touch()
+        self.init(testdir, extra_args=['--reconfigure'])
+
+        # Wipe of empty builddir should work
+        self.new_builddir()
+        self.init(testdir, extra_args=['--wipe'])
+
+        # Wipe of partial builddir should work
+        self.new_builddir()
+        Path(self.builddir, 'meson-private').mkdir()
+        Path(self.builddir, 'dummy').touch()
+        self.init(testdir, extra_args=['--wipe'])
+
+        # Wipe of not empty builddir should fail
+        self.new_builddir()
+        Path(self.builddir, 'dummy').touch()
+        with self.assertRaises(subprocess.CalledProcessError) as cm:
+            self.init(testdir, extra_args=['--wipe'])
+        self.assertIn('Directory is not empty', cm.exception.stdout)


Test patch: