Merge pull request #8200 from bonzini/mtest-asyncio-logs

mtest: improvements to logging
diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml
index 0cf9156..87fae3b 100644
--- a/.github/workflows/images.yml
+++ b/.github/workflows/images.yml
@@ -32,7 +32,7 @@
           - { name: Fedora,           id: fedora   }
           - { name: OpenSUSE,         id: opensuse }
           - { name: Ubuntu Bionic,    id: bionic   }
-          - { name: Ubuntu Eoan,      id: eoan     }
+          - { name: Ubuntu Rolling,   id: ubuntu-rolling  }
     steps:
       - uses: actions/checkout@v2
 
diff --git a/.github/workflows/os_comp.yml b/.github/workflows/os_comp.yml
index a5be499..3cdcccd 100644
--- a/.github/workflows/os_comp.yml
+++ b/.github/workflows/os_comp.yml
@@ -6,10 +6,21 @@
       - master
       # Stable branches such as 0.56 or the eventual 1.0
       - '[0-9]+.[0-9]+'
+    paths:
+      - "mesonbuild/**"
+      - "test cases/**"
+      - ".github/workflows/images.yml"
+      - ".github/workflows/os_comp.yml"
+      - "run_unittests.py"
   pull_request:
+    paths:
+      - "mesonbuild/**"
+      - "test cases/**"
+      - ".github/workflows/images.yml"
+      - ".github/workflows/os_comp.yml"
+      - "run_unittests.py"
 
 jobs:
-  arch:
     name: ${{ matrix.cfg.name }}
     runs-on: ubuntu-latest
     strategy:
@@ -30,8 +41,8 @@
       # via the `args` array ub the image.json
       run: bash -c 'source /ci/env_vars.sh; cd $GITHUB_WORKSPACE; ./run_tests.py $CI_ARGS'
 
-  eoan:
-    name: 'Ubuntu Eoan'
+  ubuntu-rolling:
+    name: 'Ubuntu Rolling'
     runs-on: ubuntu-latest
 
     strategy:
@@ -60,7 +71,7 @@
             CXX: 'g++'
 
     container:
-      image: mesonbuild/eoan
+      image: mesonbuild/ubuntu-rolling
       env:
         MESON_RSP_THRESHOLD: ${{ matrix.cfg.MESON_RSP_THRESHOLD }}
         MESON_ARGS: ${{ matrix.cfg.MESON_ARGS }}
@@ -94,4 +105,4 @@
           update-alternatives --set i686-w64-mingw32-gcc   /usr/bin/i686-w64-mingw32-gcc-posix
           update-alternatives --set i686-w64-mingw32-g++   /usr/bin/i686-w64-mingw32-g++-posix
 
-          ./run_tests.py $RUN_TESTS_ARGS -- $MESON_ARGS
\ No newline at end of file
+          ./run_tests.py $RUN_TESTS_ARGS -- $MESON_ARGS
diff --git a/ci/ciimage/arch/install.sh b/ci/ciimage/arch/install.sh
index ccbbab0..683986f 100755
--- a/ci/ciimage/arch/install.sh
+++ b/ci/ciimage/arch/install.sh
@@ -12,12 +12,12 @@
   itstool gtk3 java-environment=8 gtk-doc llvm clang sdl2 graphviz
   doxygen vulkan-validation-layers openssh mercurial gtk-sharp-2 qt5-tools
   libwmf valgrind cmake netcdf-fortran openmpi nasm gnustep-base gettext
-  python-jsonschema python-lxml
+  python-lxml hotdoc
   # cuda
 )
 
 aur_pkgs=(scalapack)
-pip_pkgs=(hotdoc gcovr)
+pip_pkgs=(gcovr)
 cleanup_pkgs=(go)
 
 AUR_USER=docker
diff --git a/ci/ciimage/eoan/image.json b/ci/ciimage/eoan/image.json
deleted file mode 100644
index cbf86c3..0000000
--- a/ci/ciimage/eoan/image.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "base_image": "ubuntu:eoan",
-  "env": {
-    "CI": "1",
-    "DC": "gdc"
-  }
-}
diff --git a/ci/ciimage/ubuntu-rolling/image.json b/ci/ciimage/ubuntu-rolling/image.json
new file mode 100644
index 0000000..038b945
--- /dev/null
+++ b/ci/ciimage/ubuntu-rolling/image.json
@@ -0,0 +1,7 @@
+{
+  "base_image": "ubuntu:rolling",
+  "env": {
+    "CI": "1",
+    "DC": "gdc"
+  }
+}
diff --git a/ci/ciimage/eoan/install.sh b/ci/ciimage/ubuntu-rolling/install.sh
similarity index 89%
rename from ci/ciimage/eoan/install.sh
rename to ci/ciimage/ubuntu-rolling/install.sh
index 36dec72..507113b 100755
--- a/ci/ciimage/eoan/install.sh
+++ b/ci/ciimage/ubuntu-rolling/install.sh
@@ -9,14 +9,12 @@
 export DC=gdc
 
 pkgs=(
-  python3-pytest-xdist
+  python3-pytest-xdist python3-jsonschema
   python3-pip libxml2-dev libxslt1-dev libyaml-dev libjson-glib-dev
   python3-lxml
   wget unzip
   qt5-default clang
   pkg-config-arm-linux-gnueabihf
-  qt4-linguist-tools
-  python-dev
   libomp-dev
   llvm lcov
   dub ldc
@@ -43,7 +41,7 @@
 eatmydata apt-get -y install "${pkgs[@]}"
 eatmydata apt-get -y install --no-install-recommends wine-stable  # Wine is special
 
-eatmydata python3 -m pip install hotdoc codecov gcovr jsonschema
+eatmydata python3 -m pip install hotdoc codecov gcovr
 
 # dub stuff
 dub_fetch urld
diff --git a/ci/ciimage/eoan/test.sh b/ci/ciimage/ubuntu-rolling/test.sh
similarity index 100%
rename from ci/ciimage/eoan/test.sh
rename to ci/ciimage/ubuntu-rolling/test.sh
diff --git a/docs/markdown/Creating-releases.md b/docs/markdown/Creating-releases.md
index 040fb53..55242a6 100644
--- a/docs/markdown/Creating-releases.md
+++ b/docs/markdown/Creating-releases.md
@@ -58,3 +58,21 @@
 So with `--no-tests` you can tell Meson "Do not build and test generated
 packages.".
 
+## Release a subproject separately
+
+*Since 0.57.0* the `meson dist` command can now create a distribution tarball
+for a subproject in the same git repository as the main project. This can be
+useful if parts of the project (e.g. libraries) can be built and distributed
+separately. In that case they can be moved into `subprojects/mysub` and running
+`meson dist` in that directory will now create a tarball containing only the
+source code from that subdir and not the rest of the main project or other
+subprojects.
+
+For example:
+```sh
+git clone https://github.com/myproject
+cd myproject/subprojects/mysubproject
+meson builddir
+meson dist -C builddir
+```
+This produces `builddir/meson-dist/mysubproject-1.0.tar.xz` tarball.
diff --git a/docs/markdown/Machine-files.md b/docs/markdown/Machine-files.md
index e703988..e553720 100644
--- a/docs/markdown/Machine-files.md
+++ b/docs/markdown/Machine-files.md
@@ -34,7 +34,7 @@
 option = false
 ```
 
-An integer must be either an unquoted numeric constant;
+An integer must be an unquoted numeric constant.
 ```ini
 option = 42
 ```
diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md
index cb8347a..3a844da 100644
--- a/docs/markdown/Reference-manual.md
+++ b/docs/markdown/Reference-manual.md
@@ -119,7 +119,9 @@
   environment juggling. *(since 0.52.0)* A dictionary is also accepted.
 - `exe_wrapper`: a list containing the wrapper command or script followed by the arguments to it
 - `gdb`: if `true`, the tests are also run under `gdb`
-- `timeout_multiplier`: a number to multiply the test timeout with
+- `timeout_multiplier`: a number to multiply the test timeout with.
+  *Since 0.57* if timeout_multiplier is `<= 0` the test has infinite duration,
+  in previous versions of Meson the test would fail with a timeout immediately.
 - `is_default` *(since 0.49.0)*: a bool to set whether this is the default test setup.
   If `true`, the setup will be used whenever `meson test` is run
   without the `--setup` option.
@@ -372,6 +374,10 @@
 - `install_mode` *(since 0.47.0)*: the file mode and optionally the
   owner/uid and group/gid
 - `output`: list of output files
+- `env` *(since 0.57.0)*: environment variables to set, such as
+  `{'NAME1': 'value1', 'NAME2': 'value2'}` or `['NAME1=value1', 'NAME2=value2']`,
+  or an [`environment()` object](#environment-object) which allows more
+  sophisticated environment juggling.
 
 The list of strings passed to the `command` keyword argument accept
 the following special string substitutions:
@@ -1426,7 +1432,9 @@
 
 - `version`: which is a free form string describing the version of
   this project. You can access the value in your Meson build files
-  with `meson.project_version()`.
+  with `meson.project_version()`. Since 0.57.0 this can also be a
+  `File` object pointing to a file that contains exactly one line of
+  text.
 
 ### run_command()
 
@@ -1730,7 +1738,8 @@
 
 - `timeout`: the amount of seconds the test is allowed to run, a test
   that exceeds its time limit is always considered failed, defaults to
-  30 seconds
+  30 seconds. *Since 0.57* if timeout is `<= 0` the test has infinite duration,
+  in previous versions of Meson the test would fail with a timeout immediately.
 
 - `workdir`: absolute path that will be used as the working directory
   for the test
@@ -1817,21 +1826,29 @@
   it from a subproject is a hard error. *(since 0.49.0)* Accepts multiple arguments
   for the fscript. *(since 0.54.0)* The `MESON_SOURCE_ROOT` and `MESON_BUILD_ROOT`
   environment variables are set when dist scripts are run.
+
   *(since 0.55.0)* The output of `configure_file`, `files`, and `find_program`
   as well as strings.
 
+  *(since 0.57.0)* `file` objects and the output of `configure_file` may be
+  *used as the `script_name` parameter.
+
 - `add_install_script(script_name, arg1, arg2, ...)`: causes the script
   given as an argument to be run during the install step, this script
   will have the environment variables `MESON_SOURCE_ROOT`,
   `MESON_BUILD_ROOT`, `MESON_INSTALL_PREFIX`,
   `MESON_INSTALL_DESTDIR_PREFIX`, and `MESONINTROSPECT` set.
   All positional arguments are passed as parameters.
+
+  *(since 0.54.0)* If `meson install` is called with the `--quiet` option, the
+  environment variable `MESON_INSTALL_QUIET` will be set.
+
   *(since 0.55.0)* The output of `configure_file`, `files`, `find_program`,
   `custom_target`, indexes of `custom_target`, `executable`, `library`, and
   other built targets as well as strings.
 
-  *(since 0.54.0)* If `meson install` is called with the `--quiet` option, the
-  environment variable `MESON_INSTALL_QUIET` will be set.
+  *(since 0.57.0)* `file` objects and the output of `configure_file` may be
+  *used as the `script_name` parameter.
 
   Meson uses the `DESTDIR` environment variable as set by the
   inherited environment to determine the (temporary) installation
@@ -1859,9 +1876,13 @@
   executable given as an argument after all project files have been
   generated. This script will have the environment variables
   `MESON_SOURCE_ROOT` and `MESON_BUILD_ROOT` set.
+
   *(since 0.55.0)* The output of `configure_file`, `files`, and `find_program`
   as well as strings.
 
+  *(since 0.57.0)* `file` objects and the output of `configure_file` may be
+  *used as the `script_name` parameter.
+
 - `backend()` *(since 0.37.0)*: returns a string representing the
   current backend: `ninja`, `vs2010`, `vs2015`, `vs2017`, `vs2019`,
   or `xcode`.
diff --git a/docs/markdown/Syntax.md b/docs/markdown/Syntax.md
index 65cb70f..4f17ac8 100644
--- a/docs/markdown/Syntax.md
+++ b/docs/markdown/Syntax.md
@@ -657,7 +657,7 @@
 equality_operator: "==" | "!="
 expression: assignment_expression
 expression_list: expression ("," expression)*
-expression_statememt: expression
+expression_statement: expression
 function_expression: id_expression "(" [argument_list] ")"
 hex_literal: "0x" HEX_NUMBER
 HEX_NUMBER: /[a-fA-F0-9]+/
diff --git a/docs/markdown/snippets/customtarget_env.md b/docs/markdown/snippets/customtarget_env.md
new file mode 100644
index 0000000..93687ab
--- /dev/null
+++ b/docs/markdown/snippets/customtarget_env.md
@@ -0,0 +1,11 @@
+## custom_target() now accepts `env` keyword argument
+
+Environment variables can now be passed to `custom_target()` command.
+
+```meson
+env = environment()
+env.append('PATH', '/foo')
+custom_target(..., env: env)
+custom_target(..., env: {'MY_ENV': 'value'})
+custom_target(..., env: ['MY_ENV=value'])
+```
diff --git a/docs/markdown/snippets/dist_subproject.md b/docs/markdown/snippets/dist_subproject.md
new file mode 100644
index 0000000..2520ccd
--- /dev/null
+++ b/docs/markdown/snippets/dist_subproject.md
@@ -0,0 +1,16 @@
+## Package a subproject
+
+The `meson dist` command can now create a distribution tarball for a subproject
+in the same git repository as the main project. This can be useful if parts of
+the project (e.g. libraries) can be built and distributed separately. In that
+case they can be moved into `subprojects/mysub` and running `meson dist` in that
+directory will now create a tarball containing only the source code from that
+subdir and not the rest of the main project or other subprojects.
+
+For example:
+```sh
+git clone https://github.com/myproject
+cd myproject/subprojects/mysubproject
+meson builddir
+meson dist -C builddir
+```
diff --git a/docs/markdown/snippets/pass_file_to_add_script.md b/docs/markdown/snippets/pass_file_to_add_script.md
new file mode 100644
index 0000000..10d08e0
--- /dev/null
+++ b/docs/markdown/snippets/pass_file_to_add_script.md
@@ -0,0 +1,15 @@
+## The `add_*_script` methods now accept a File as the first argument
+
+Meson now accepts `file` objects, including those produced by
+`configure_file` as the `prog` (first) parameter of the various
+`add_*_script` methods
+
+```meson
+install_script = configure_file(
+  configuration : conf,
+  input : 'myscript.py.in',
+  output : 'myscript.py',
+)
+
+meson.add_install_script(install_script, other, params)
+```
diff --git a/docs/markdown/snippets/test_timeout.md b/docs/markdown/snippets/test_timeout.md
new file mode 100644
index 0000000..e436d2e
--- /dev/null
+++ b/docs/markdown/snippets/test_timeout.md
@@ -0,0 +1,9 @@
+## `test()` timeout and timeout_multiplier value <= 0
+
+`test(..., timeout: 0)`, or negative value, used to abort the test immediately
+but now instead allow infinite duration. Note that omitting the `timeout`
+keyword argument still defaults to 30s timeout.
+
+Likewise, `add_test_setup(..., timeout_multiplier: 0)`, or
+`meson test --timeout-multiplier 0`, or negative value, disable tests timeout.
+
diff --git a/docs/markdown/snippets/versionfile.md b/docs/markdown/snippets/versionfile.md
new file mode 100644
index 0000000..6071f2c
--- /dev/null
+++ b/docs/markdown/snippets/versionfile.md
@@ -0,0 +1,12 @@
+## Project version can be specified with a file
+
+Meson can be instructed to load project's version string from an
+external file like this:
+
+```meson
+project('foo', 'c' version: files('VERSION'))
+```
+
+The version file must contain exactly one line of text and that will
+be set as the project's version. If the line ends in a newline
+character, it is removed.
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
index ced1d7d..caec761 100644
--- a/mesonbuild/backend/backends.py
+++ b/mesonbuild/backend/backends.py
@@ -14,6 +14,7 @@
 
 from collections import OrderedDict
 from functools import lru_cache
+from itertools import chain
 from pathlib import Path
 import enum
 import json
@@ -21,9 +22,10 @@
 import pickle
 import re
 import shlex
-import subprocess
 import textwrap
 import typing as T
+import hashlib
+import copy
 
 from .. import build
 from .. import dependencies
@@ -44,6 +46,11 @@
     InstallType = T.List[T.Tuple[str, str, T.Optional['FileMode']]]
     InstallSubdirsType = T.List[T.Tuple[str, str, T.Optional['FileMode'], T.Tuple[T.Set[str], T.Set[str]]]]
 
+# Languages that can mix with C or C++ but don't support unity builds yet
+# because the syntax we use for unity builds is specific to C/++/ObjC/++.
+# Assembly files cannot be unitified and neither can LLVM IR files
+LANGS_CANT_UNITY = ('d', 'fortran')
+
 class TestProtocol(enum.Enum):
 
     EXITCODE = 0
@@ -99,7 +106,7 @@
         self.data: 'InstallType' = []
         self.po_package_name: str = ''
         self.po = []
-        self.install_scripts: T.List[build.RunScript] = []
+        self.install_scripts: T.List[ExecutableSerialisation] = []
         self.install_subdirs: 'InstallSubdirsType' = []
         self.mesonintrospect = mesonintrospect
         self.version = version
@@ -119,10 +126,10 @@
         self.optional = optional
 
 class ExecutableSerialisation:
-    def __init__(self, cmd_args, env=None, exe_wrapper=None,
+    def __init__(self, cmd_args, env: T.Optional[build.EnvironmentVariables] = None, exe_wrapper=None,
                  workdir=None, extra_paths=None, capture=None) -> None:
         self.cmd_args = cmd_args
-        self.env = env or {}
+        self.env = env
         if exe_wrapper is not None:
             assert(isinstance(exe_wrapper, dependencies.ExternalProgram))
         self.exe_runner = exe_wrapper
@@ -371,12 +378,11 @@
                 raise MesonException('Unknown data type in object list.')
         return obj_list
 
-    def as_meson_exe_cmdline(self, tname, exe, cmd_args, workdir=None,
-                             extra_bdeps=None, capture=None, force_serialize=False):
-        '''
-        Serialize an executable for running with a generator or a custom target
-        '''
-        import hashlib
+    def get_executable_serialisation(self, cmd, workdir=None,
+                                     extra_bdeps=None, capture=None,
+                                     env: T.Optional[build.EnvironmentVariables] = None):
+        exe = cmd[0]
+        cmd_args = cmd[1:]
         if isinstance(exe, dependencies.ExternalProgram):
             exe_cmd = exe.get_command()
             exe_for_machine = exe.for_machine
@@ -396,11 +402,10 @@
         is_cross_built = not self.environment.machines.matches_build_machine(exe_for_machine)
         if is_cross_built and self.environment.need_exe_wrapper():
             exe_wrapper = self.environment.get_exe_wrapper()
-            if not exe_wrapper.found():
-                msg = 'The exe_wrapper {!r} defined in the cross file is ' \
-                      'needed by target {!r}, but was not found. Please ' \
-                      'check the command and/or add it to PATH.'
-                raise MesonException(msg.format(exe_wrapper.name, tname))
+            if not exe_wrapper or not exe_wrapper.found():
+                msg = 'An exe_wrapper is needed but was not found. Please define one ' \
+                      'in cross file and check the command and/or add it to PATH.'
+                raise MesonException(msg)
         else:
             if exe_cmd[0].endswith('.jar'):
                 exe_cmd = ['java', '-jar'] + exe_cmd
@@ -408,11 +413,24 @@
                 exe_cmd = ['mono'] + exe_cmd
             exe_wrapper = None
 
+        workdir = workdir or self.environment.get_build_dir()
+        return ExecutableSerialisation(exe_cmd + cmd_args, env,
+                                       exe_wrapper, workdir,
+                                       extra_paths, capture)
+
+    def as_meson_exe_cmdline(self, tname, exe, cmd_args, workdir=None,
+                             extra_bdeps=None, capture=None, force_serialize=False,
+                             env: T.Optional[build.EnvironmentVariables] = None):
+        '''
+        Serialize an executable for running with a generator or a custom target
+        '''
+        cmd = [exe] + cmd_args
+        es = self.get_executable_serialisation(cmd, workdir, extra_bdeps, capture, env)
         reasons = []
-        if extra_paths:
+        if es.extra_paths:
             reasons.append('to set PATH')
 
-        if exe_wrapper:
+        if es.exe_runner:
             reasons.append('to use exe_wrapper')
 
         if workdir:
@@ -421,6 +439,9 @@
         if any('\n' in c for c in cmd_args):
             reasons.append('because command contains newlines')
 
+        if env and env.varnames:
+            reasons.append('to set env')
+
         force_serialize = force_serialize or bool(reasons)
 
         if capture:
@@ -430,11 +451,9 @@
             if not capture:
                 return None, ''
             return ((self.environment.get_build_command() +
-                    ['--internal', 'exe', '--capture', capture, '--'] + exe_cmd + cmd_args),
+                    ['--internal', 'exe', '--capture', capture, '--'] + es.cmd_args),
                     ', '.join(reasons))
 
-        workdir = workdir or self.environment.get_build_dir()
-        env = {}
         if isinstance(exe, (dependencies.ExternalProgram,
                             build.BuildTarget, build.CustomTarget)):
             basename = exe.name
@@ -445,15 +464,12 @@
         # Take a digest of the cmd args, env, workdir, and capture. This avoids
         # collisions and also makes the name deterministic over regenerations
         # which avoids a rebuild by Ninja because the cmdline stays the same.
-        data = bytes(str(sorted(env.items())) + str(cmd_args) + str(workdir) + str(capture),
+        data = bytes(str(env) + str(cmd_args) + str(es.workdir) + str(capture),
                      encoding='utf-8')
         digest = hashlib.sha1(data).hexdigest()
         scratch_file = 'meson_exe_{0}_{1}.dat'.format(basename, digest)
         exe_data = os.path.join(self.environment.get_scratch_dir(), scratch_file)
         with open(exe_data, 'wb') as f:
-            es = ExecutableSerialisation(exe_cmd + cmd_args, env,
-                                         exe_wrapper, workdir,
-                                         extra_paths, capture)
             pickle.dump(es, f)
         return (self.environment.get_build_command() + ['--internal', 'exe', '--unpickle', exe_data],
                 ', '.join(reasons))
@@ -636,6 +652,9 @@
             unity_size = self.get_option_for_target(OptionKey('unity_size'), extobj.target)
 
             for comp, srcs in compsrcs.items():
+                if comp.language in LANGS_CANT_UNITY:
+                    sources += srcs
+                    continue
                 for i in range(len(srcs) // unity_size + 1):
                     osrc = self.get_unity_source_file(extobj.target,
                                                       comp.get_default_suffix(), i)
@@ -767,7 +786,7 @@
             # pkg-config puts the thread flags itself via `Cflags:`
         # Fortran requires extra include directives.
         if compiler.language == 'fortran':
-            for lt in target.link_targets:
+            for lt in chain(target.link_targets, target.link_whole_targets):
                 priv_dir = self.get_target_private_dir(lt)
                 commands += compiler.get_include_args(priv_dir, False)
         return commands
@@ -1174,16 +1193,16 @@
         return inputs, outputs, cmd
 
     def run_postconf_scripts(self) -> None:
+        from ..scripts.meson_exe import run_exe
         env = {'MESON_SOURCE_ROOT': self.environment.get_source_dir(),
                'MESON_BUILD_ROOT': self.environment.get_build_dir(),
                'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in self.environment.get_build_command() + ['introspect']]),
                }
-        child_env = os.environ.copy()
-        child_env.update(env)
 
         for s in self.build.postconf_scripts:
-            cmd = s['exe'] + s['args']
-            subprocess.check_call(cmd, env=child_env)
+            name = ' '.join(s.cmd_args)
+            mlog.log('Running postconf script {!r}'.format(name))
+            run_exe(s, env)
 
     def create_install_data(self) -> InstallData:
         strip_bin = self.environment.lookup_binary_entry(MachineChoice.HOST, 'strip')
@@ -1312,18 +1331,18 @@
                         d.targets.append(i)
 
     def generate_custom_install_script(self, d: InstallData) -> None:
-        result: T.List[build.RunScript] = []
+        result: T.List[ExecutableSerialisation] = []
         srcdir = self.environment.get_source_dir()
         builddir = self.environment.get_build_dir()
         for i in self.build.install_scripts:
-            exe = i['exe']
-            args = i['args']
             fixed_args = []
-            for a in args:
+            for a in i.cmd_args:
                 a = a.replace('@SOURCE_ROOT@', srcdir)
                 a = a.replace('@BUILD_ROOT@', builddir)
                 fixed_args.append(a)
-            result.append(build.RunScript(exe, fixed_args))
+            es = copy.copy(i)
+            es.cmd_args = fixed_args
+            result.append(es)
         d.install_scripts = result
 
     def generate_header_install(self, d: InstallData) -> None:
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
index 400433f..36f1fd2 100644
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -626,19 +626,14 @@
             srcs[f] = s
         return srcs
 
-    # Languages that can mix with C or C++ but don't support unity builds yet
-    # because the syntax we use for unity builds is specific to C/++/ObjC/++.
-    # Assembly files cannot be unitified and neither can LLVM IR files
-    langs_cant_unity = ('d', 'fortran')
-
     def get_target_source_can_unity(self, target, source):
         if isinstance(source, File):
             source = source.fname
         if self.environment.is_llvm_ir(source) or \
            self.environment.is_assembly(source):
             return False
-        suffix = os.path.splitext(source)[1][1:]
-        for lang in self.langs_cant_unity:
+        suffix = os.path.splitext(source)[1][1:].lower()
+        for lang in backends.LANGS_CANT_UNITY:
             if lang not in target.compilers:
                 continue
             if suffix in target.compilers[lang].file_suffixes:
@@ -762,7 +757,7 @@
         if is_unity:
             # Warn about incompatible sources if a unity build is enabled
             langs = set(target.compilers.keys())
-            langs_cant = langs.intersection(self.langs_cant_unity)
+            langs_cant = langs.intersection(backends.LANGS_CANT_UNITY)
             if langs_cant:
                 langs_are = langs = ', '.join(langs_cant).upper()
                 langs_are += ' are' if len(langs_cant) > 1 else ' is'
@@ -918,7 +913,7 @@
         all_suffixes = set(compilers.lang_suffixes['cpp']) | set(compilers.lang_suffixes['fortran'])
         selected_sources = []
         for source in compiled_sources:
-            ext = os.path.splitext(source)[1][1:]
+            ext = os.path.splitext(source)[1][1:].lower()
             if ext in all_suffixes:
                 selected_sources.append(source)
         return selected_sources
@@ -965,7 +960,8 @@
 
         meson_exe_cmd, reason = self.as_meson_exe_cmdline(target.name, target.command[0], cmd[1:],
                                                           extra_bdeps=target.get_transitive_build_target_deps(),
-                                                          capture=ofilenames[0] if target.capture else None)
+                                                          capture=ofilenames[0] if target.capture else None,
+                                                          env=target.env)
         if meson_exe_cmd:
             cmd = meson_exe_cmd
             cmd_type = ' (wrapped by meson {})'.format(reason)
@@ -1598,7 +1594,7 @@
                 if isinstance(g, GeneratedList):
                     fname = os.path.join(self.get_target_private_dir(target), i)
                 else:
-                    fname = i
+                    fname = os.path.join(g.get_subdir(), i)
                 if main_rust_file is None:
                     main_rust_file = fname
                 orderdeps.append(fname)
@@ -2602,7 +2598,10 @@
     def get_fortran_orderdeps(self, target, compiler):
         if compiler.language != 'fortran':
             return []
-        return [os.path.join(self.get_target_dir(lt), lt.get_filename()) for lt in target.link_targets]
+        return [
+            os.path.join(self.get_target_dir(lt), lt.get_filename())
+            for lt in itertools.chain(target.link_targets, target.link_whole_targets)
+        ]
 
     def generate_msvc_pch_command(self, target, compiler, pch):
         header = pch[0]
@@ -3026,14 +3025,14 @@
 
     def generate_gcov_clean(self):
         gcno_elem = NinjaBuildElement(self.all_outputs, 'meson-clean-gcno', 'CUSTOM_COMMAND', 'PHONY')
-        gcno_elem.add_item('COMMAND', mesonlib.meson_command + ['--internal', 'delwithsuffix', '.', 'gcno'])
+        gcno_elem.add_item('COMMAND', mesonlib.get_meson_command() + ['--internal', 'delwithsuffix', '.', 'gcno'])
         gcno_elem.add_item('description', 'Deleting gcno files')
         self.add_build(gcno_elem)
         # Alias that runs the target defined above
         self.create_target_alias('meson-clean-gcno')
 
         gcda_elem = NinjaBuildElement(self.all_outputs, 'meson-clean-gcda', 'CUSTOM_COMMAND', 'PHONY')
-        gcda_elem.add_item('COMMAND', mesonlib.meson_command + ['--internal', 'delwithsuffix', '.', 'gcda'])
+        gcda_elem.add_item('COMMAND', mesonlib.get_meson_command() + ['--internal', 'delwithsuffix', '.', 'gcda'])
         gcda_elem.add_item('description', 'Deleting gcda files')
         self.add_build(gcda_elem)
         # Alias that runs the target defined above
diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py
index 6e070a7..c47fb4a 100644
--- a/mesonbuild/backend/vs2010backend.py
+++ b/mesonbuild/backend/vs2010backend.py
@@ -176,7 +176,11 @@
             # x86
             self.platform = 'Win32'
         elif target_machine == 'aarch64' or target_machine == 'arm64':
-            self.platform = 'arm64'
+            target_cpu = self.interpreter.builtin['target_machine'].cpu_method(None, None)
+            if target_cpu == 'arm64ec':
+                self.platform = 'arm64ec'
+            else:
+                self.platform = 'arm64'
         elif 'arm' in target_machine.lower():
             self.platform = 'ARM'
         else:
@@ -572,7 +576,8 @@
                                                    workdir=tdir_abs,
                                                    extra_bdeps=extra_bdeps,
                                                    capture=ofilenames[0] if target.capture else None,
-                                                   force_serialize=True)
+                                                   force_serialize=True,
+                                                   env=target.env)
         if target.build_always_stale:
             # Use a nonexistent file to always consider the target out-of-date.
             ofilenames += [self.nonexistent_file(os.path.join(self.environment.get_scratch_dir(),
@@ -1218,6 +1223,8 @@
             targetmachine.text = 'MachineARM'
         elif targetplatform == 'arm64':
             targetmachine.text = 'MachineARM64'
+        elif targetplatform == 'arm64ec':
+            targetmachine.text = 'MachineARM64EC'
         else:
             raise MesonException('Unsupported Visual Studio target machine: ' + targetplatform)
         # /nologo
diff --git a/mesonbuild/backend/xcodebackend.py b/mesonbuild/backend/xcodebackend.py
index 0e39c65..230b684 100644
--- a/mesonbuild/backend/xcodebackend.py
+++ b/mesonbuild/backend/xcodebackend.py
@@ -578,7 +578,7 @@
         self.write_line(');')
         self.write_line('runOnlyForDeploymentPostprocessing = 0;')
         self.write_line('shellPath = /bin/sh;')
-        cmd = mesonlib.meson_command + ['test', test_data, '-C', self.environment.get_build_dir()]
+        cmd = mesonlib.get_meson_command() + ['test', test_data, '-C', self.environment.get_build_dir()]
         cmdstr = ' '.join(["'%s'" % i for i in cmd])
         self.write_line('shellScript = "%s";' % cmdstr)
         self.write_line('showEnvVarsInLog = 0;')
@@ -734,7 +734,9 @@
                 else:
                     product_name = target.get_basename()
                 ldargs += target.link_args
+                cargs = []
                 for dep in target.get_external_deps():
+                    cargs += dep.get_compile_args()
                     ldargs += dep.get_link_args()
                 ldstr = ' '.join(ldargs)
                 valid = self.buildconfmap[target_name][buildtype]
@@ -751,6 +753,7 @@
                     args = pargs + gargs + targs
                     if args:
                         langargs[langnamemap[lang]] = args
+                langargs['C'] += cargs
                 symroot = os.path.join(self.environment.get_build_dir(), target.subdir)
                 self.write_line('%s /* %s */ = {' % (valid, buildtype))
                 self.indent_level += 1
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
index 13783d1..32daf54 100644
--- a/mesonbuild/build.py
+++ b/mesonbuild/build.py
@@ -2153,6 +2153,7 @@
         'build_by_default',
         'override_options',
         'console',
+        'env',
     ])
 
     def __init__(self, name, subdir, subproject, kwargs, absolute_paths=False, backend=None):
@@ -2325,6 +2326,7 @@
             else:
                 mlog.debug(i)
                 raise InvalidArguments('Unknown type {!r} in depend_files.'.format(type(i).__name__))
+        self.env = kwargs.get('env')
 
     def get_dependencies(self):
         return self.dependencies
@@ -2607,14 +2609,6 @@
         else:
             self.rename = rename
 
-class RunScript(dict):
-    def __init__(self, script, args):
-        super().__init__()
-        assert(isinstance(script, list))
-        assert(isinstance(args, list))
-        self['exe'] = script
-        self['args'] = args
-
 class TestSetup:
     def __init__(self, exe_wrapper: T.Optional[T.List[str]], gdb: bool,
                  timeout_multiplier: int, env: EnvironmentVariables):
diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py
index 782b7c2..abb4983 100644
--- a/mesonbuild/cmake/interpreter.py
+++ b/mesonbuild/cmake/interpreter.py
@@ -1322,7 +1322,7 @@
 
             # Generate the command list
             command = []  # type: T.List[T.Union[str, IdNode, IndexNode]]
-            command += mesonlib.meson_command
+            command += mesonlib.get_meson_command()
             command += ['--internal', 'cmake_run_ctgt']
             command += ['-o', '@OUTPUT@']
             if tgt.original_outputs:
diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py
index c11319b..0a2d478 100644
--- a/mesonbuild/compilers/c.py
+++ b/mesonbuild/compilers/c.py
@@ -16,7 +16,8 @@
 import typing as T
 
 from .. import coredata
-from ..mesonlib import MachineChoice, MesonException, mlog, version_compare, OptionKey
+from .. import mlog
+from ..mesonlib import MachineChoice, MesonException, version_compare, OptionKey
 from .c_function_attributes import C_FUNC_ATTRIBUTES
 from .mixins.clike import CLikeCompiler
 from .mixins.ccrx import CcrxCompiler
diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py
index 1a7f3f1..219b0f2 100644
--- a/mesonbuild/environment.py
+++ b/mesonbuild/environment.py
@@ -867,7 +867,7 @@
         # re-initialized with project options by the interpreter during
         # build file parsing.
         # meson_command is used by the regenchecker script, which runs meson
-        self.coredata = coredata.CoreData(options, self.scratch_dir, mesonlib.meson_command)
+        self.coredata = coredata.CoreData(options, self.scratch_dir, mesonlib.get_meson_command())
         self.first_invocation = True
 
     def is_cross_build(self, when_building_for: MachineChoice = MachineChoice.HOST) -> bool:
@@ -887,7 +887,7 @@
         return self.coredata
 
     def get_build_command(self, unbuffered=False):
-        cmd = mesonlib.meson_command[:]
+        cmd = mesonlib.get_meson_command().copy()
         if unbuffered and 'python' in os.path.basename(cmd[0]):
             cmd.insert(1, '-u')
         return cmd
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
index a3fa050..ceaa29a 100644
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -1,4 +1,4 @@
-# Copyright 2012-2019 The Meson development team
+# Copyright 2012-2021 The Meson development team
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
@@ -34,7 +34,7 @@
 from .interpreterbase import TYPE_var, TYPE_nkwargs
 from .modules import ModuleReturnValue, ExtensionModule
 from .cmake import CMakeInterpreter
-from .backend.backends import TestProtocol, Backend
+from .backend.backends import TestProtocol, Backend, ExecutableSerialisation
 
 from pathlib import Path, PurePath
 import os
@@ -1947,27 +1947,27 @@
                              'backend': self.backend_method,
                              })
 
-    def _find_source_script(self, prog: T.Union[str, ExecutableHolder], args):
-        if isinstance(prog, ExecutableHolder):
-            prog_path = self.interpreter.backend.get_target_filename(prog.held_object)
-            return build.RunScript([prog_path], args)
-        elif isinstance(prog, ExternalProgramHolder):
-            return build.RunScript(prog.get_command(), args)
-
+    def _find_source_script(self, prog: T.Union[str, mesonlib.File, ExecutableHolder], args):
+        if isinstance(prog, (ExecutableHolder, ExternalProgramHolder)):
+            return self.interpreter.backend.get_executable_serialisation([unholder(prog)] + args)
         # Prefer scripts in the current source directory
         search_dir = os.path.join(self.interpreter.environment.source_dir,
                                   self.interpreter.subdir)
         key = (prog, search_dir)
         if key in self._found_source_scripts:
             found = self._found_source_scripts[key]
+        elif isinstance(prog, mesonlib.File):
+            prog = prog.rel_to_builddir(self.interpreter.environment.source_dir)
+            found = dependencies.ExternalProgram(prog, search_dir=self.interpreter.environment.build_dir)
         else:
             found = dependencies.ExternalProgram(prog, search_dir=search_dir)
-            if found.found():
-                self._found_source_scripts[key] = found
-            else:
-                m = 'Script or command {!r} not found or not executable'
-                raise InterpreterException(m.format(prog))
-        return build.RunScript(found.get_command(), args)
+
+        if found.found():
+            self._found_source_scripts[key] = found
+        else:
+            m = 'Script or command {!r} not found or not executable'
+            raise InterpreterException(m.format(prog))
+        return self.interpreter.backend.get_executable_serialisation([found] + args)
 
     def _process_script_args(
             self, name: str, args: T.List[T.Union[
@@ -2016,9 +2016,12 @@
         return script_args
 
     @permittedKwargs(set())
-    def add_install_script_method(self, args: 'T.Tuple[T.Union[str, ExecutableHolder], T.Union[str, mesonlib.File, CustomTargetHolder, CustomTargetIndexHolder, ConfigureFileHolder], ...]', kwargs):
+    def add_install_script_method(self, args: 'T.Tuple[T.Union[str, mesonlib.File, ExecutableHolder], T.Union[str, mesonlib.File, CustomTargetHolder, CustomTargetIndexHolder, ConfigureFileHolder], ...]', kwargs):
         if len(args) < 1:
             raise InterpreterException('add_install_script takes one or more arguments')
+        if isinstance(args[0], mesonlib.File):
+            FeatureNew.single_use('Passing file object to script parameter of add_install_script',
+                                  '0.57.0', self.interpreter.subproject)
         script_args = self._process_script_args('add_install_script', args[1:], allow_built=True)
         script = self._find_source_script(args[0], script_args)
         self.build.install_scripts.append(script)
@@ -2027,6 +2030,9 @@
     def add_postconf_script_method(self, args, kwargs):
         if len(args) < 1:
             raise InterpreterException('add_postconf_script takes one or more arguments')
+        if isinstance(args[0], mesonlib.File):
+            FeatureNew.single_use('Passing file object to script parameter of add_postconf_script',
+                                  '0.57.0', self.interpreter.subproject)
         script_args = self._process_script_args('add_postconf_script', args[1:], allow_built=True)
         script = self._find_source_script(args[0], script_args)
         self.build.postconf_scripts.append(script)
@@ -2038,6 +2044,9 @@
         if len(args) > 1:
             FeatureNew.single_use('Calling "add_dist_script" with multiple arguments',
                                   '0.49.0', self.interpreter.subproject)
+        if isinstance(args[0], mesonlib.File):
+            FeatureNew.single_use('Passing file object to script parameter of add_dist_script',
+                                  '0.57.0', self.interpreter.subproject)
         if self.interpreter.subproject != '':
             raise InterpreterException('add_dist_script may not be used in a subproject.')
         script_args = self._process_script_args('add_dist_script', args[1:], allow_built=True)
@@ -2315,7 +2324,8 @@
                                       'depfile',
                                       'build_by_default',
                                       'build_always_stale',
-                                      'console'},
+                                      'console',
+                                      'env'},
                     'dependency': {'default_options',
                                    'embed',
                                    'fallback',
@@ -2544,7 +2554,7 @@
             return GeneratedListHolder(item)
         elif isinstance(item, build.RunTarget):
             raise RuntimeError('This is not a pipe.')
-        elif isinstance(item, build.RunScript):
+        elif isinstance(item, ExecutableSerialisation):
             raise RuntimeError('Do not do this.')
         elif isinstance(item, build.Data):
             return DataHolder(item)
@@ -2571,7 +2581,7 @@
                 self.module_method_callback(v)
             elif isinstance(v, build.GeneratedList):
                 pass
-            elif isinstance(v, build.RunScript):
+            elif isinstance(v, ExecutableSerialisation):
                 self.build.install_scripts.append(v)
             elif isinstance(v, build.Data):
                 self.build.data.append(v)
@@ -3168,9 +3178,29 @@
         if not self.is_subproject():
             self.build.project_name = proj_name
         self.active_projectname = proj_name
-        self.project_version = kwargs.get('version', 'undefined')
-        if not isinstance(self.project_version, str):
-            raise InvalidCode('The version keyword argument must be a string.')
+        version = kwargs.get('version', 'undefined')
+        if isinstance(version, list):
+            if len(version) != 1:
+                raise InvalidCode('Version argument is an array with more than one entry.')
+            version = version[0]
+        if isinstance(version, mesonlib.File):
+            FeatureNew.single_use('version from file', '0.57.0', self.subproject)
+            self.add_build_def_file(version)
+            ifname = version.absolute_path(self.environment.source_dir,
+                                           self.environment.build_dir)
+            try:
+                ver_data = Path(ifname).read_text(encoding='utf-8').split('\n')
+            except FileNotFoundError:
+                raise InterpreterException('Version file not found.')
+            if len(ver_data) == 2 and ver_data[1] == '':
+                ver_data = ver_data[0:1]
+            if len(ver_data) != 1:
+                raise InterpreterException('Version file must contain exactly one line of text.')
+            self.project_version = ver_data[0]
+        elif isinstance(version, str):
+            self.project_version = version
+        else:
+            raise InvalidCode('The version keyword argument must be a string or a file.')
         if self.build.project_version is None:
             self.build.project_version = self.project_version
         proj_license = mesonlib.stringlistify(kwargs.get('license', 'unknown'))
@@ -3997,6 +4027,7 @@
         raise SubdirDoneRequest()
 
     @stringArgs
+    @FeatureNewKwargs('custom_target', '0.57.0', ['env'])
     @FeatureNewKwargs('custom_target', '0.48.0', ['console'])
     @FeatureNewKwargs('custom_target', '0.47.0', ['install_mode', 'build_always_stale'])
     @FeatureNewKwargs('custom_target', '0.40.0', ['build_by_default'])
@@ -4018,6 +4049,7 @@
             except mesonlib.MesonException:
                 mlog.warning('''Custom target input \'%s\' can\'t be converted to File object(s).
 This will become a hard error in the future.''' % kwargs['input'], location=self.current_node)
+        kwargs['env'] = self.unpack_env_kwarg(kwargs)
         tg = CustomTargetHolder(build.CustomTarget(name, self.subdir, self.subproject, kwargs, backend=self.backend), self)
         self.add_target(name, tg.held_object)
         return tg
@@ -4140,6 +4172,10 @@
         if not isinstance(should_fail, bool):
             raise InterpreterException('Keyword argument should_fail must be a boolean.')
         timeout = kwargs.get('timeout', 30)
+        if not isinstance(timeout, int):
+            raise InterpreterException('Timeout must be an integer.')
+        if timeout <= 0:
+            FeatureNew('test() timeout <= 0', '0.57.0').use(self.subproject)
         if 'workdir' in kwargs:
             workdir = kwargs['workdir']
             if not isinstance(workdir, str):
@@ -4148,8 +4184,6 @@
                 raise InterpreterException('Workdir keyword argument must be an absolute path.')
         else:
             workdir = None
-        if not isinstance(timeout, int):
-            raise InterpreterException('Timeout must be an integer.')
         protocol = kwargs.get('protocol', 'exitcode')
         if protocol not in {'exitcode', 'tap', 'gtest', 'rust'}:
             raise InterpreterException('Protocol must be one of "exitcode", "tap", "gtest", or "rust".')
@@ -4638,6 +4672,8 @@
         timeout_multiplier = kwargs.get('timeout_multiplier', 1)
         if not isinstance(timeout_multiplier, int):
             raise InterpreterException('Timeout multiplier must be a number.')
+        if timeout_multiplier <= 0:
+            FeatureNew('add_test_setup() timeout_multiplier <= 0', '0.57.0').use(self.subproject)
         is_default = kwargs.get('is_default', False)
         if not isinstance(is_default, bool):
             raise InterpreterException('is_default option must be a boolean')
diff --git a/mesonbuild/mdist.py b/mesonbuild/mdist.py
index 3ca13e5..4547b38 100644
--- a/mesonbuild/mdist.py
+++ b/mesonbuild/mdist.py
@@ -23,9 +23,10 @@
 from glob import glob
 from pathlib import Path
 from mesonbuild.environment import detect_ninja
-from mesonbuild.mesonlib import windows_proof_rmtree, MesonException
+from mesonbuild.mesonlib import windows_proof_rmtree, MesonException, quiet_git
 from mesonbuild.wrap import wrap
 from mesonbuild import mlog, build
+from .scripts.meson_exe import run_exe
 
 archive_choices = ['gztar', 'xztar', 'zip']
 archive_extension = {'gztar': '.tar.gz',
@@ -79,26 +80,36 @@
 
 def run_dist_scripts(src_root, bld_root, dist_root, dist_scripts):
     assert(os.path.isabs(dist_root))
-    env = os.environ.copy()
+    env = {}
     env['MESON_DIST_ROOT'] = dist_root
     env['MESON_SOURCE_ROOT'] = src_root
     env['MESON_BUILD_ROOT'] = bld_root
     for d in dist_scripts:
-        script = d['exe']
-        args = d['args']
-        name = ' '.join(script + args)
+        name = ' '.join(d.cmd_args)
         print('Running custom dist script {!r}'.format(name))
         try:
-            rc = subprocess.call(script + args, env=env)
+            rc = run_exe(d, env)
             if rc != 0:
                 sys.exit('Dist script errored out')
         except OSError:
             print('Failed to run dist script {!r}'.format(name))
             sys.exit(1)
 
+def git_root(src_root):
+    # Cannot use --show-toplevel here because git in our CI prints cygwin paths
+    # that python cannot resolve. Workaround this by taking parent of src_root.
+    prefix = quiet_git(['rev-parse', '--show-prefix'], src_root, check=True)[1].strip()
+    if not prefix:
+        return Path(src_root)
+    prefix_level = len(Path(prefix).parents)
+    return Path(src_root).parents[prefix_level - 1]
+
 def is_git(src_root):
-    _git = os.path.join(src_root, '.git')
-    return os.path.isdir(_git) or os.path.isfile(_git)
+    '''
+    Checks if meson.build file at the root source directory is tracked by git.
+    It could be a subproject part of the parent project git repository.
+    '''
+    return quiet_git(['ls-files', '--error-unmatch', 'meson.build'], src_root)[0]
 
 def git_have_dirty_index(src_root):
     '''Check whether there are uncommitted changes in git'''
@@ -109,9 +120,21 @@
     if git_have_dirty_index(src_root):
         mlog.warning('Repository has uncommitted changes that will not be included in the dist tarball')
     if os.path.exists(distdir):
-        shutil.rmtree(distdir)
-    os.makedirs(distdir)
-    subprocess.check_call(['git', 'clone', '--shared', src_root, distdir])
+        windows_proof_rmtree(distdir)
+    repo_root = git_root(src_root)
+    if repo_root.samefile(src_root):
+        os.makedirs(distdir)
+        subprocess.check_call(['git', 'clone', '--shared', src_root, distdir])
+    else:
+        subdir = Path(src_root).relative_to(repo_root)
+        tmp_distdir = distdir + '-tmp'
+        if os.path.exists(tmp_distdir):
+            windows_proof_rmtree(tmp_distdir)
+        os.makedirs(tmp_distdir)
+        subprocess.check_call(['git', 'clone', '--shared', '--no-checkout', str(repo_root), tmp_distdir])
+        subprocess.check_call(['git', 'checkout', 'HEAD', '--', str(subdir)], cwd=tmp_distdir)
+        Path(tmp_distdir, subdir).rename(distdir)
+        windows_proof_rmtree(tmp_distdir)
     process_submodules(distdir)
     del_gitfiles(distdir)
 
@@ -133,7 +156,7 @@
         compressed_name = distdir + archive_extension[a]
         shutil.make_archive(distdir, a, root_dir=dist_sub, base_dir=dist_name)
         output_names.append(compressed_name)
-    shutil.rmtree(distdir)
+    windows_proof_rmtree(distdir)
     return output_names
 
 def is_hg(src_root):
@@ -245,7 +268,7 @@
     b = build.load(options.wd)
     # This import must be load delayed, otherwise it will get the default
     # value of None.
-    from mesonbuild.mesonlib import meson_command
+    from mesonbuild.mesonlib import get_meson_command
     src_root = b.environment.source_dir
     bld_root = b.environment.build_dir
     priv_dir = os.path.join(bld_root, 'meson-private')
@@ -279,7 +302,7 @@
     rc = 0
     if not options.no_tests:
         # Check only one.
-        rc = check_dist(names[0], meson_command, extra_meson_args, bld_root, priv_dir)
+        rc = check_dist(names[0], get_meson_command(), extra_meson_args, bld_root, priv_dir)
     if rc == 0:
         for name in names:
             create_hash(name)
diff --git a/mesonbuild/mesonlib/__init__.py b/mesonbuild/mesonlib/__init__.py
new file mode 100644
index 0000000..5b646b5
--- /dev/null
+++ b/mesonbuild/mesonlib/__init__.py
@@ -0,0 +1,30 @@
+# SPDX-license-identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helper functions and classes."""
+
+import os
+
+from .universal import *
+
+# Here we import either the posix implementations, the windows implementations,
+# or a generic no-op implementation
+if os.name == 'posix':
+    from .posix import *
+elif os.name == 'nt':
+    from .win32 import *
+else:
+    from .platform import *
diff --git a/mesonbuild/mesonlib/platform.py b/mesonbuild/mesonlib/platform.py
new file mode 100644
index 0000000..cdd42b1
--- /dev/null
+++ b/mesonbuild/mesonlib/platform.py
@@ -0,0 +1,37 @@
+# SPDX-license-identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""base classes providing no-op functionality.."""
+
+import os
+import typing as T
+
+from .. import mlog
+
+__all__ = ['BuildDirLock']
+
+# This needs to be inheritted by the specific implementations to make type
+# checking happy
+class BuildDirLock:
+
+    def __init__(self, builddir: str) -> None:
+        self.lockfilename = os.path.join(builddir, 'meson-private/meson.lock')
+
+    def __enter__(self) -> None:
+        mlog.debug('Calling ther no-op version of BuildDirLock')
+
+    def __exit__(self, *args: T.Any) -> None:
+        pass
diff --git a/mesonbuild/mesonlib/posix.py b/mesonbuild/mesonlib/posix.py
new file mode 100644
index 0000000..1d8ba8c
--- /dev/null
+++ b/mesonbuild/mesonlib/posix.py
@@ -0,0 +1,39 @@
+# SPDX-license-identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Posix specific implementations of mesonlib functionality."""
+
+import fcntl
+import typing as T
+
+from .universal import MesonException
+from .platform import BuildDirLock as BuildDirLockBase
+
+__all__ = ['BuildDirLock']
+
+class BuildDirLock(BuildDirLockBase):
+
+    def __enter__(self) -> None:
+        self.lockfile = open(self.lockfilename, 'w')
+        try:
+            fcntl.flock(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
+        except (BlockingIOError, PermissionError):
+            self.lockfile.close()
+            raise MesonException('Some other Meson process is already using this build directory. Exiting.')
+
+    def __exit__(self, *args: T.Any) -> None:
+        fcntl.flock(self.lockfile, fcntl.LOCK_UN)
+        self.lockfile.close()
diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib/universal.py
similarity index 95%
rename from mesonbuild/mesonlib.py
rename to mesonbuild/mesonlib/universal.py
index ef48ec2..dfcec8e 100644
--- a/mesonbuild/mesonlib.py
+++ b/mesonbuild/mesonlib/universal.py
@@ -30,34 +30,117 @@
 from mesonbuild import mlog
 
 if T.TYPE_CHECKING:
-    from .build import ConfigurationData
-    from .coredata import KeyedOptionDictType, UserOption
-    from .compilers.compilers import CompilerType
-    from .interpreterbase import ObjectHolder
+    from ..build import ConfigurationData
+    from ..coredata import KeyedOptionDictType, UserOption
+    from ..compilers.compilers import CompilerType
+    from ..interpreterbase import ObjectHolder
 
-    FileOrString = T.Union['File', str]
+FileOrString = T.Union['File', str]
 
 _T = T.TypeVar('_T')
 _U = T.TypeVar('_U')
 
-have_fcntl = False
-have_msvcrt = False
+__all__ = [
+    'GIT',
+    'an_unpicklable_object',
+    'python_command',
+    'project_meson_versions',
+    'File',
+    'FileMode',
+    'GitException',
+    'LibType',
+    'MachineChoice',
+    'MesonException',
+    'EnvironmentException',
+    'FileOrString',
+    'GitException',
+    'OptionKey',
+    'dump_conf_header',
+    'OptionOverrideProxy',
+    'OptionProxy',
+    'OptionType',
+    'OrderedSet',
+    'PerMachine',
+    'PerMachineDefaultable',
+    'PerThreeMachine',
+    'PerThreeMachineDefaultable',
+    'ProgressBar',
+    'TemporaryDirectoryWinProof',
+    'Version',
+    'check_direntry_issues',
+    'classify_unity_sources',
+    'current_vs_supports_modules',
+    'darwin_get_object_archs',
+    'default_libdir',
+    'default_libexecdir',
+    'default_prefix',
+    'detect_subprojects',
+    'detect_vcs',
+    'do_conf_file',
+    'do_conf_str',
+    'do_define',
+    'do_replacement',
+    'exe_exists',
+    'expand_arguments',
+    'extract_as_list',
+    'get_compiler_for_source',
+    'get_filenames_templates_dict',
+    'get_library_dirs',
+    'get_variable_regex',
+    'get_wine_shortpath',
+    'git',
+    'has_path_sep',
+    'is_aix',
+    'is_android',
+    'is_ascii_string',
+    'is_cygwin',
+    'is_debianlike',
+    'is_dragonflybsd',
+    'is_freebsd',
+    'is_haiku',
+    'is_hurd',
+    'is_irix',
+    'is_linux',
+    'is_netbsd',
+    'is_openbsd',
+    'is_osx',
+    'is_qnx',
+    'is_sunos',
+    'is_windows',
+    'iter_regexin_iter',
+    'join_args',
+    'listify',
+    'partition',
+    'path_is_in_root',
+    'Popen_safe',
+    'quiet_git',
+    'quote_arg',
+    'relative_to_if_possible',
+    'relpath',
+    'replace_if_different',
+    'run_once',
+    'get_meson_command',
+    'set_meson_command',
+    'split_args',
+    'stringlistify',
+    'substitute_values',
+    'substring_is_in_list',
+    'typeslistify',
+    'unholder',
+    'verbose_git',
+    'version_compare',
+    'version_compare_condition_with_min',
+    'version_compare_many',
+    'windows_proof_rm',
+    'windows_proof_rmtree',
+]
+
+
 # TODO: this is such a hack, this really should be either in coredata or in the
 # interpreter
 # {subproject: project_meson_version}
 project_meson_versions = collections.defaultdict(str)  # type: T.DefaultDict[str, str]
 
-try:
-    import fcntl
-    have_fcntl = True
-except Exception:
-    pass
-
-try:
-    import msvcrt
-    have_msvcrt = True
-except Exception:
-    pass
 
 from glob import glob
 
@@ -66,7 +149,7 @@
     python_command = [sys.executable, 'runpython']
 else:
     python_command = [sys.executable]
-meson_command = None
+_meson_command = None
 
 class MesonException(Exception):
     '''Exceptions thrown by Meson'''
@@ -117,20 +200,24 @@
 
 def set_meson_command(mainfile: str) -> None:
     global python_command
-    global meson_command
+    global _meson_command
     # On UNIX-like systems `meson` is a Python script
     # On Windows `meson` and `meson.exe` are wrapper exes
     if not mainfile.endswith('.py'):
-        meson_command = [mainfile]
+        _meson_command = [mainfile]
     elif os.path.isabs(mainfile) and mainfile.endswith('mesonmain.py'):
         # Can't actually run meson with an absolute path to mesonmain.py, it must be run as -m mesonbuild.mesonmain
-        meson_command = python_command + ['-m', 'mesonbuild.mesonmain']
+        _meson_command = python_command + ['-m', 'mesonbuild.mesonmain']
     else:
         # Either run uninstalled, or full path to meson-script.py
-        meson_command = python_command + [mainfile]
+        _meson_command = python_command + [mainfile]
     # We print this value for unit tests.
     if 'MESON_COMMAND_TESTS' in os.environ:
-        mlog.log('meson_command is {!r}'.format(meson_command))
+        mlog.log('meson_command is {!r}'.format(_meson_command))
+
+
+def get_meson_command() -> T.Optional[T.List[str]]:
+    return _meson_command
 
 
 def is_ascii_string(astring: T.Union[str, bytes]) -> bool:
@@ -261,8 +348,15 @@
             perms |= stat.S_ISVTX
         return perms
 
+dot_C_dot_H_warning = """You are using .C or .H files in your project. This is deprecated.
+         Currently, Meson treats this as C code, but this
+            might change in the future, breaking your build.
+         You code also might be already broken on gcc and clang.
+         See https://github.com/mesonbuild/meson/pull/8239 for the discussions."""
 class File:
     def __init__(self, is_built: bool, subdir: str, fname: str):
+        if fname.endswith(".C") or fname.endswith(".H"):
+            mlog.warning(dot_C_dot_H_warning, once=True)
         self.is_built = is_built
         self.subdir = subdir
         self.fname = fname
@@ -1564,29 +1658,6 @@
     def difference(self, set_: T.Union[T.Set[_T], 'OrderedSet[_T]']) -> 'OrderedSet[_T]':
         return type(self)(e for e in self if e not in set_)
 
-class BuildDirLock:
-
-    def __init__(self, builddir: str) -> None:
-        self.lockfilename = os.path.join(builddir, 'meson-private/meson.lock')
-
-    def __enter__(self) -> None:
-        self.lockfile = open(self.lockfilename, 'w')
-        try:
-            if have_fcntl:
-                fcntl.flock(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
-            elif have_msvcrt:
-                msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
-        except (BlockingIOError, PermissionError):
-            self.lockfile.close()
-            raise MesonException('Some other Meson process is already using this build directory. Exiting.')
-
-    def __exit__(self, *args: T.Any) -> None:
-        if have_fcntl:
-            fcntl.flock(self.lockfile, fcntl.LOCK_UN)
-        elif have_msvcrt:
-            msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
-        self.lockfile.close()
-
 def relpath(path: str, start: str) -> str:
     # On Windows a relative path can't be evaluated for paths on two different
     # drives (i.e. c:\foo and f:\bar).  The only thing left to do is to use the
@@ -1965,7 +2036,7 @@
             raw3 = raw2
             for_machine = MachineChoice.HOST
 
-        from .compilers import all_languages
+        from ..compilers import all_languages
         if any(raw3.startswith(f'{l}_') for l in all_languages):
             lang, opt = raw3.split('_', 1)
         else:
@@ -2024,4 +2095,4 @@
 
     def is_base(self) -> bool:
         """Convenience method to check if this is a base option."""
-        return self.type is OptionType.BASE
\ No newline at end of file
+        return self.type is OptionType.BASE
diff --git a/mesonbuild/mesonlib/win32.py b/mesonbuild/mesonlib/win32.py
new file mode 100644
index 0000000..0919ef7
--- /dev/null
+++ b/mesonbuild/mesonlib/win32.py
@@ -0,0 +1,39 @@
+# SPDX-license-identifier: Apache-2.0
+# Copyright 2012-2021 The Meson development team
+# Copyright © 2021 Intel Corporation
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Windows specific implementations of mesonlib functionality."""
+
+import msvcrt
+import typing as T
+
+from .universal import MesonException
+from .platform import BuildDirLock as BuildDirLockBase
+
+__all__ = ['BuildDirLock']
+
+class BuildDirLock(BuildDirLockBase):
+
+    def __enter__(self) -> None:
+        self.lockfile = open(self.lockfilename, 'w')
+        try:
+            msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
+        except (BlockingIOError, PermissionError):
+            self.lockfile.close()
+            raise MesonException('Some other Meson process is already using this build directory. Exiting.')
+
+    def __exit__(self, *args: T.Any) -> None:
+        msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
+        self.lockfile.close()
diff --git a/mesonbuild/minit.py b/mesonbuild/minit.py
index 4a38313..55e716c 100644
--- a/mesonbuild/minit.py
+++ b/mesonbuild/minit.py
@@ -174,7 +174,7 @@
             print('Build directory already exists, deleting it.')
             shutil.rmtree(options.builddir)
         print('Building...')
-        cmd = mesonlib.meson_command + [options.builddir]
+        cmd = mesonlib.get_meson_command() + [options.builddir]
         ret = subprocess.run(cmd)
         if ret.returncode:
             raise SystemExit
diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py
index 3e425eb..98608c7 100644
--- a/mesonbuild/minstall.py
+++ b/mesonbuild/minstall.py
@@ -30,6 +30,7 @@
 from .coredata import version as coredata_version
 from .mesonlib import is_windows, Popen_safe
 from .scripts import depfixer, destdir_join
+from .scripts.meson_exe import run_exe
 try:
     from __main__ import __file__ as main_file
 except ImportError:
@@ -485,17 +486,12 @@
         if self.options.quiet:
             env['MESON_INSTALL_QUIET'] = '1'
 
-        child_env = os.environ.copy()
-        child_env.update(env)
-
         for i in d.install_scripts:
             self.did_install_something = True  # Custom script must report itself if it does nothing.
-            script = i['exe']
-            args = i['args']
-            name = ' '.join(script + args)
+            name = ' '.join(i.cmd_args)
             self.log('Running custom install script {!r}'.format(name))
             try:
-                rc = subprocess.call(script + args, env=child_env)
+                rc = run_exe(i, env)
             except OSError:
                 print('FAILED: install script \'{}\' could not be run, stopped'.format(name))
                 # POSIX shells return 127 when a command could not be found
diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py
index 5cad9f5..f564eb4 100644
--- a/mesonbuild/modules/gnome.py
+++ b/mesonbuild/modules/gnome.py
@@ -899,7 +899,7 @@
             args.append('--media=' + '@@'.join(media))
         if langs:
             args.append('--langs=' + '@@'.join(langs))
-        inscript = build.RunScript(script, args)
+        inscript = state.backend.get_executable_serialisation(script + args)
 
         potargs = state.environment.get_build_command() + [
             '--internal', 'yelphelper', 'pot',
@@ -1051,7 +1051,7 @@
             self.interpreter.add_test(state.current_node, check_args, check_kwargs, True)
         res = [custom_target, alias_target]
         if kwargs.get('install', True):
-            res.append(build.RunScript(command, args))
+            res.append(state.backend.get_executable_serialisation(command + args))
         return ModuleReturnValue(custom_target, res)
 
     def _get_build_args(self, kwargs, state, depends):
diff --git a/mesonbuild/modules/hotdoc.py b/mesonbuild/modules/hotdoc.py
index 931db12..eda411c 100644
--- a/mesonbuild/modules/hotdoc.py
+++ b/mesonbuild/modules/hotdoc.py
@@ -350,7 +350,7 @@
 
         install_script = None
         if install is True:
-            install_script = HotdocRunScript(self.build_command, [
+            install_script = self.state.backend.get_executable_serialisation(self.build_command + [
                 "--internal", "hotdoc",
                 "--install", os.path.join(fullname, 'html'),
                 '--name', self.name,
@@ -391,11 +391,6 @@
         return res
 
 
-class HotdocRunScript(build.RunScript):
-    def __init__(self, script, args):
-        super().__init__(script, args)
-
-
 class HotDocModule(ExtensionModule):
     @FeatureNew('Hotdoc Module', '0.48.0')
     def __init__(self, interpreter):
diff --git a/mesonbuild/modules/i18n.py b/mesonbuild/modules/i18n.py
index d48f83b..ae24e6e 100644
--- a/mesonbuild/modules/i18n.py
+++ b/mesonbuild/modules/i18n.py
@@ -180,7 +180,7 @@
                     pkg_arg]
             if lang_arg:
                 args.append(lang_arg)
-            iscript = build.RunScript(script, args)
+            iscript = state.backend.get_executable_serialisation(script + args)
             targets.append(iscript)
 
         return ModuleReturnValue(None, targets)
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py
index b1d43bd..24db5ce 100644
--- a/mesonbuild/mtest.py
+++ b/mesonbuild/mtest.py
@@ -130,7 +130,7 @@
     parser.add_argument('-t', '--timeout-multiplier', type=float, default=None,
                         help='Define a multiplier for test timeout, for example '
                         ' when running tests in particular conditions they might take'
-                        ' more time to execute.')
+                        ' more time to execute. (<= 0 to disable timeout)')
     parser.add_argument('--setup', default=None, dest='setup',
                         help='Which test setup to use.')
     parser.add_argument('--test-args', default=[], type=split_args,
@@ -541,7 +541,7 @@
             spaces=' ' * TestResult.maxlen(),
             dur=int(time.time() - self.progress_test.starttime),
             durlen=harness.duration_max_len,
-            timeout=int(self.progress_test.timeout))
+            timeout=int(self.progress_test.timeout or -1))
         detail = self.progress_test.detail
         if detail:
             right += '   ' + detail
@@ -1270,12 +1270,14 @@
         if ('MALLOC_PERTURB_' not in env or not env['MALLOC_PERTURB_']) and not options.benchmark:
             env['MALLOC_PERTURB_'] = str(random.randint(1, 255))
 
-        if self.options.gdb or self.test.timeout is None:
+        if self.options.gdb or self.test.timeout is None or self.test.timeout <= 0:
             timeout = None
-        elif self.options.timeout_multiplier is not None:
-            timeout = self.test.timeout * self.options.timeout_multiplier
-        else:
+        elif self.options.timeout_multiplier is None:
             timeout = self.test.timeout
+        elif self.options.timeout_multiplier <= 0:
+            timeout = None
+        else:
+            timeout = self.test.timeout * self.options.timeout_multiplier
 
         self.runobj = TestRun(test, env, name, timeout)
 
diff --git a/mesonbuild/scripts/depscan.py b/mesonbuild/scripts/depscan.py
index df7df48..c85f8e7 100644
--- a/mesonbuild/scripts/depscan.py
+++ b/mesonbuild/scripts/depscan.py
@@ -46,7 +46,7 @@
         self.sources_with_exports = [] # type: T.List[str]
 
     def scan_file(self, fname: str) -> None:
-        suffix = os.path.splitext(fname)[1][1:]
+        suffix = os.path.splitext(fname)[1][1:].lower()
         if suffix in lang_suffixes['fortran']:
             self.scan_fortran_file(fname)
         elif suffix in lang_suffixes['cpp']:
@@ -131,7 +131,7 @@
         return objname
 
     def module_name_for(self, src: str) -> str:
-        suffix= os.path.splitext(src)[1][1:]
+        suffix = os.path.splitext(src)[1][1:].lower()
         if suffix in lang_suffixes['fortran']:
             exported = self.exports[src]
             # Module foo:bar goes to a file name foo@bar.smod
diff --git a/mesonbuild/scripts/meson_exe.py b/mesonbuild/scripts/meson_exe.py
index 50ad2f5..620f579 100644
--- a/mesonbuild/scripts/meson_exe.py
+++ b/mesonbuild/scripts/meson_exe.py
@@ -30,7 +30,7 @@
     parser.add_argument('--capture')
     return parser
 
-def run_exe(exe: ExecutableSerialisation) -> int:
+def run_exe(exe: ExecutableSerialisation, extra_env: T.Optional[dict] = None) -> int:
     if exe.exe_runner:
         if not exe.exe_runner.found():
             raise AssertionError('BUG: Can\'t run cross-compiled exe {!r} with not-found '
@@ -39,7 +39,10 @@
     else:
         cmd_args = exe.cmd_args
     child_env = os.environ.copy()
-    child_env.update(exe.env)
+    if extra_env:
+        child_env.update(extra_env)
+    if exe.env:
+        child_env = exe.env.get_env(child_env)
     if exe.extra_paths:
         child_env['PATH'] = (os.pathsep.join(exe.extra_paths + ['']) +
                              child_env['PATH'])
@@ -55,14 +58,21 @@
                          stderr=subprocess.PIPE)
     stdout, stderr = p.communicate()
 
-    if exe.pickled and p.returncode != 0:
-        print('while executing {!r}'.format(cmd_args))
-
     if p.returncode == 0xc0000135:
         # STATUS_DLL_NOT_FOUND on Windows indicating a common problem that is otherwise hard to diagnose
         raise FileNotFoundError('due to missing DLLs')
 
-    if exe.capture and p.returncode == 0:
+    if p.returncode != 0:
+        if exe.pickled:
+            print('while executing {!r}'.format(cmd_args))
+        if not exe.capture:
+            print('--- stdout ---')
+            print(stdout.decode())
+        print('--- stderr ---')
+        print(stderr.decode())
+        return p.returncode
+
+    if exe.capture:
         skip_write = False
         try:
             with open(exe.capture, 'rb') as cur:
@@ -72,11 +82,8 @@
         if not skip_write:
             with open(exe.capture, 'wb') as output:
                 output.write(stdout)
-    else:
-        sys.stdout.buffer.write(stdout)
-    if stderr:
-        sys.stderr.buffer.write(stderr)
-    return p.returncode
+
+    return 0
 
 def run(args: T.List[str]) -> int:
     global options
diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py
index 34e58e3..cce44a8 100644
--- a/mesonbuild/wrap/wrap.py
+++ b/mesonbuild/wrap/wrap.py
@@ -353,8 +353,10 @@
             raise WrapException(m)
 
     def resolve_git_submodule(self) -> bool:
+        # Is git installed? If not, we're probably not in a git repository and
+        # definitely cannot try to conveniently set up a submodule.
         if not GIT:
-            raise WrapException('Git program not found.')
+            return False
         # Are we in a git repository?
         ret, out = quiet_git(['rev-parse'], self.subdir_root)
         if not ret:
diff --git a/run_mypy.py b/run_mypy.py
index daf7431..e6900c7 100755
--- a/run_mypy.py
+++ b/run_mypy.py
@@ -1,9 +1,10 @@
 #!/usr/bin/env python3
 
-import sys
-import subprocess
-import argparse
 from pathlib import Path
+import argparse
+import os
+import subprocess
+import sys
 import typing as T
 
 modules = [
@@ -24,7 +25,8 @@
     'mesonbuild/interpreterbase.py',
     'mesonbuild/linkers.py',
     'mesonbuild/mcompile.py',
-    'mesonbuild/mesonlib.py',
+    'mesonbuild/mesonlib/platform.py',
+    'mesonbuild/mesonlib/universal.py',
     'mesonbuild/minit.py',
     'mesonbuild/minstall.py',
     'mesonbuild/mintro.py',
@@ -40,6 +42,11 @@
     'tools'
 ]
 
+if os.name == 'posix':
+    modules.append('mesonbuild/mesonlib/posix.py')
+elif os.name == 'nt':
+    modules.append('mesonbuild/mesonlib/win32.py')
+
 def check_mypy() -> None:
     try:
         import mypy
diff --git a/run_unittests.py b/run_unittests.py
index 8762b9c..2a14f78 100755
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -30,7 +30,7 @@
 import io
 import operator
 import threading
-import zipfile
+import zipfile, tarfile
 import hashlib
 from itertools import chain
 from unittest import mock
@@ -146,6 +146,9 @@
                            'user.name', 'Author Person'], cwd=project_dir)
     subprocess.check_call(['git', 'config',
                            'user.email', 'teh_coderz@example.com'], cwd=project_dir)
+    _git_add_all(project_dir)
+
+def _git_add_all(project_dir):
     subprocess.check_call('git add *', cwd=project_dir, shell=True,
                           stdout=subprocess.DEVNULL)
     subprocess.check_call(['git', 'commit', '-a', '-m', 'I am a project'], cwd=project_dir,
@@ -2847,7 +2850,7 @@
             raise unittest.SkipTest('Dist is only supported with Ninja')
 
         try:
-            self.dist_impl(_git_init)
+            self.dist_impl(_git_init, _git_add_all)
         except PermissionError:
             # When run under Windows CI, something (virus scanner?)
             # holds on to the git files so cleaning up the dir
@@ -2916,10 +2919,10 @@
         path = os.path.join(project_dir, 'subprojects', name)
         os.makedirs(path)
         with open(os.path.join(path, 'meson.build'), 'w') as ofile:
-            ofile.write("project('{}')".format(name))
+            ofile.write("project('{}', version: '1.0')".format(name))
         return path
 
-    def dist_impl(self, vcs_init, include_subprojects=True):
+    def dist_impl(self, vcs_init, vcs_add_all=None, include_subprojects=True):
         # Create this on the fly because having rogue .git directories inside
         # the source tree leads to all kinds of trouble.
         with tempfile.TemporaryDirectory() as project_dir:
@@ -2930,6 +2933,7 @@
                     test('dist test', e)
                     subproject('vcssub', required : false)
                     subproject('tarballsub', required : false)
+                    subproject('samerepo', required : false)
                     '''))
             with open(os.path.join(project_dir, 'distexe.c'), 'w') as ofile:
                 ofile.write(textwrap.dedent('''\
@@ -2949,6 +2953,8 @@
                 vcs_init(self.create_dummy_subproject(project_dir, 'vcssub'))
                 self.create_dummy_subproject(project_dir, 'tarballsub')
                 self.create_dummy_subproject(project_dir, 'unusedsub')
+            if vcs_add_all:
+                vcs_add_all(self.create_dummy_subproject(project_dir, 'samerepo'))
             self.init(project_dir)
             self.build('dist')
             self.assertPathExists(xz_distfile)
@@ -2961,24 +2967,46 @@
             self.assertPathExists(zip_checksumfile)
 
             if include_subprojects:
+                # Verify that without --include-subprojects we have files from
+                # the main project and also files from subprojects part of the
+                # main vcs repository.
                 z = zipfile.ZipFile(zip_distfile)
-                self.assertEqual(sorted(['disttest-1.4.3/',
-                                         'disttest-1.4.3/meson.build',
-                                         'disttest-1.4.3/distexe.c']),
+                expected = ['disttest-1.4.3/',
+                            'disttest-1.4.3/meson.build',
+                            'disttest-1.4.3/distexe.c']
+                if vcs_add_all:
+                    expected += ['disttest-1.4.3/subprojects/',
+                                 'disttest-1.4.3/subprojects/samerepo/',
+                                 'disttest-1.4.3/subprojects/samerepo/meson.build']
+                self.assertEqual(sorted(expected),
                                  sorted(z.namelist()))
-
+                # Verify that with --include-subprojects we now also have files
+                # from tarball and separate vcs subprojects. But not files from
+                # unused subprojects.
                 self._run(self.meson_command + ['dist', '--formats', 'zip', '--include-subprojects'],
                           workdir=self.builddir)
                 z = zipfile.ZipFile(zip_distfile)
-                self.assertEqual(sorted(['disttest-1.4.3/',
-                                         'disttest-1.4.3/subprojects/',
-                                         'disttest-1.4.3/meson.build',
-                                         'disttest-1.4.3/distexe.c',
-                                         'disttest-1.4.3/subprojects/tarballsub/',
-                                         'disttest-1.4.3/subprojects/vcssub/',
-                                         'disttest-1.4.3/subprojects/tarballsub/meson.build',
-                                         'disttest-1.4.3/subprojects/vcssub/meson.build']),
+                expected += ['disttest-1.4.3/subprojects/tarballsub/',
+                             'disttest-1.4.3/subprojects/tarballsub/meson.build',
+                             'disttest-1.4.3/subprojects/vcssub/',
+                             'disttest-1.4.3/subprojects/vcssub/meson.build']
+                self.assertEqual(sorted(expected),
                                  sorted(z.namelist()))
+            if vcs_add_all:
+                # Verify we can distribute separately subprojects in the same vcs
+                # repository as the main project.
+                subproject_dir = os.path.join(project_dir, 'subprojects', 'samerepo')
+                self.new_builddir()
+                self.init(subproject_dir)
+                self.build('dist')
+                xz_distfile = os.path.join(self.distdir, 'samerepo-1.0.tar.xz')
+                xz_checksumfile = xz_distfile + '.sha256sum'
+                self.assertPathExists(xz_distfile)
+                self.assertPathExists(xz_checksumfile)
+                tar = tarfile.open(xz_distfile, "r:xz")
+                self.assertEqual(sorted(['samerepo-1.0',
+                                         'samerepo-1.0/meson.build']),
+                                 sorted([i.name for i in tar]))
 
     def test_rpath_uses_ORIGIN(self):
         '''
@@ -5185,6 +5213,12 @@
             os.utime(str(cmakefile))
         self.assertReconfiguredBuildIsNoop()
 
+    def test_version_file(self):
+        srcdir = os.path.join(self.common_test_dir, '2 cpp')
+        self.init(srcdir)
+        projinfo = self.introspect('--projectinfo')
+        self.assertEqual(projinfo['version'], '1.0.0')
+
 
 class FailureTests(BasePlatformTests):
     '''
diff --git a/setup.py b/setup.py
index 70a76e5..17a00b3 100644
--- a/setup.py
+++ b/setup.py
@@ -33,6 +33,7 @@
             'mesonbuild.compilers',
             'mesonbuild.compilers.mixins',
             'mesonbuild.dependencies',
+            'mesonbuild.mesonlib',
             'mesonbuild.modules',
             'mesonbuild.scripts',
             'mesonbuild.templates',
diff --git a/test cases/common/2 cpp/VERSION b/test cases/common/2 cpp/VERSION
new file mode 100644
index 0000000..3eefcb9
--- /dev/null
+++ b/test cases/common/2 cpp/VERSION
@@ -0,0 +1 @@
+1.0.0
diff --git a/test cases/common/2 cpp/meson.build b/test cases/common/2 cpp/meson.build
index de8b98e..47cb7c5 100644
--- a/test cases/common/2 cpp/meson.build
+++ b/test cases/common/2 cpp/meson.build
@@ -1,4 +1,4 @@
-project('c++ test', 'cpp')
+project('c++ test', 'cpp', version: files('VERSION'))
 
 cpp = meson.get_compiler('cpp')
 if cpp.get_id() == 'intel'
diff --git a/test cases/common/50 custom target/meson.build b/test cases/common/50 custom target/meson.build
index 5c7cfae..52e8630 100644
--- a/test cases/common/50 custom target/meson.build
+++ b/test cases/common/50 custom target/meson.build
@@ -15,6 +15,7 @@
 output : 'data.dat',
 input : 'data_source.txt',
 command : [python, comp, '--input=@INPUT@', '--output=@OUTPUT@', useless],
+env: {'MY_COMPILER_ENV': 'value'},
 install : true,
 install_dir : 'subdir'
 )
diff --git a/test cases/common/50 custom target/my_compiler.py b/test cases/common/50 custom target/my_compiler.py
index f46d23a..9869111 100755
--- a/test cases/common/50 custom target/my_compiler.py
+++ b/test cases/common/50 custom target/my_compiler.py
@@ -8,6 +8,7 @@
 args = sys.argv[:-1]
 
 if __name__ == '__main__':
+    assert os.environ['MY_COMPILER_ENV'] == 'value'
     if len(args) != 3 or not args[1].startswith('--input') or \
        not args[2].startswith('--output'):
         print(args[0], '--input=input_file --output=output_file')
diff --git a/test cases/common/54 install script/meson.build b/test cases/common/54 install script/meson.build
index 696e3f6..24d5dc8 100644
--- a/test cases/common/54 install script/meson.build
+++ b/test cases/common/54 install script/meson.build
@@ -1,6 +1,5 @@
 project('custom install script', 'c')
 
-executable('prog', 'prog.c', install : true)
 meson.add_install_script('myinstall.py', 'diiba/daaba', 'file.dat')
 meson.add_install_script('myinstall.py', 'this/should', 'also-work.dat')
 
@@ -25,3 +24,22 @@
 
 meson.add_install_script('myinstall.py', 'customtarget', t, '--mode=copy')
 meson.add_install_script('myinstall.py', 'customtargetindex', t[0], '--mode=copy')
+
+installer = configure_file(
+  input : 'myinstall.py',
+  output : 'myinstall_copy.py',
+  copy : true,
+)
+
+meson.add_install_script(installer, 'otherdir', afile, '--mode=copy')
+
+# This executable links on a library built in src/ directory. On Windows this
+# means meson must add src/ into $PATH to find the DLL when running it as
+# install script.
+myexe = executable('prog', 'prog.c',
+  link_with: mylib,
+  install : true,
+)
+if meson.can_run_host_binaries()
+  meson.add_install_script(myexe)
+endif
diff --git a/test cases/common/54 install script/myinstall.py b/test cases/common/54 install script/myinstall.py
old mode 100644
new mode 100755
diff --git a/test cases/common/54 install script/prog.c b/test cases/common/54 install script/prog.c
index 3bbf08e..85f8df9 100644
--- a/test cases/common/54 install script/prog.c
+++ b/test cases/common/54 install script/prog.c
@@ -1,6 +1,14 @@
 #include<stdio.h>
 
+#ifdef _WIN32
+  #define DO_IMPORT __declspec(dllimport)
+#else
+  #define DO_IMPORT
+#endif
+
+DO_IMPORT int foo(void);
+
 int main(void) {
     printf("This is text.\n");
-    return 0;
+    return foo();
 }
diff --git a/test cases/common/54 install script/src/foo.c b/test cases/common/54 install script/src/foo.c
new file mode 100644
index 0000000..46cb845
--- /dev/null
+++ b/test cases/common/54 install script/src/foo.c
@@ -0,0 +1,10 @@
+#ifdef _WIN32
+  #define DO_EXPORT __declspec(dllexport)
+#else
+  #define DO_EXPORT
+#endif
+
+DO_EXPORT int foo(void)
+{
+  return 0;
+}
diff --git a/test cases/common/54 install script/src/meson.build b/test cases/common/54 install script/src/meson.build
index 123fe18..72de346 100644
--- a/test cases/common/54 install script/src/meson.build
+++ b/test cases/common/54 install script/src/meson.build
@@ -1,3 +1,5 @@
 meson.add_install_script('myinstall.py', 'this/does', 'something-different.dat')
 
 afile = files('a file.txt')
+
+mylib = shared_library('mylib', 'foo.c')
diff --git a/test cases/common/54 install script/test.json b/test cases/common/54 install script/test.json
index 3804fe1..7ac2607 100644
--- a/test cases/common/54 install script/test.json
+++ b/test cases/common/54 install script/test.json
@@ -7,6 +7,7 @@
     {"type": "file", "file": "usr/this/does/something-different.dat.in"},
     {"type": "file", "file": "usr/dir/a file.txt"},
     {"type": "file", "file": "usr/dir/conf.txt"},
+    {"type": "file", "file": "usr/otherdir/a file.txt"},
     {"type": "file", "file": "usr/customtarget/1.txt"},
     {"type": "file", "file": "usr/customtarget/2.txt"},
     {"type": "file", "file": "usr/customtargetindex/1.txt"}
diff --git a/test cases/fortran/21 install static/main.f90 b/test cases/fortran/21 install static/main.f90
new file mode 100644
index 0000000..c83a6a0
--- /dev/null
+++ b/test cases/fortran/21 install static/main.f90
@@ -0,0 +1,4 @@
+use main_lib
+implicit none
+call main_hello()
+end program
\ No newline at end of file
diff --git a/test cases/fortran/21 install static/main_lib.f90 b/test cases/fortran/21 install static/main_lib.f90
new file mode 100644
index 0000000..5f3cb45
--- /dev/null
+++ b/test cases/fortran/21 install static/main_lib.f90
@@ -0,0 +1,16 @@
+module main_lib
+    
+  use static_hello
+  implicit none
+
+  private
+  public :: main_hello
+
+  contains
+
+  subroutine main_hello
+    call static_say_hello()
+    print *, "Main hello routine finished."
+  end subroutine main_hello
+
+end module main_lib
diff --git a/test cases/fortran/21 install static/meson.build b/test cases/fortran/21 install static/meson.build
new file mode 100644
index 0000000..a91613f
--- /dev/null
+++ b/test cases/fortran/21 install static/meson.build
@@ -0,0 +1,20 @@
+# Based on 'fortran/5 static', but:
+#   - Uses a subproject dependency
+#   - Is an install:true static library to trigger certain codepath (promotion to link_whole)
+#   - Does fortran code 'generation' with configure_file
+#   - Uses .F90 ext (capital F typically denotes a dependence on preprocessor treatment, which however is not used)
+project('try-static-subproject-dependency', 'fortran', default_options: ['default_library=static'])
+
+static_dep = dependency('static_hello', fallback: ['static_hello', 'static_hello_dep'])
+
+mainsrc = 'main_lib.f90'
+mainsrc = configure_file(
+    command: [find_program('cp'), '@INPUT@', '@OUTPUT@'],
+    input: mainsrc,
+    output: 'main_lib_output.F90'
+)
+main_lib = library('mainstatic', mainsrc, dependencies: static_dep, install: true)
+main_dep = declare_dependency(link_with: main_lib)
+
+main_exe = executable('main_exe', 'main.f90', dependencies: main_dep)
+test('static_subproject_test', main_exe)
diff --git a/test cases/fortran/21 install static/subprojects/static_hello/meson.build b/test cases/fortran/21 install static/subprojects/static_hello/meson.build
new file mode 100644
index 0000000..7edca39
--- /dev/null
+++ b/test cases/fortran/21 install static/subprojects/static_hello/meson.build
@@ -0,0 +1,12 @@
+project('static-hello', 'fortran')
+
+# staticlibsource = 'static_hello.f90'
+staticlibsource = configure_file(
+    command: [find_program('cp'), '@INPUT@', '@OUTPUT@'],
+    input: 'static_hello.f90',
+    output: 'static_hello_output.F90'
+)
+
+static_hello_lib = static_library('static_hello', staticlibsource, install: false)
+
+static_hello_dep = declare_dependency(link_with: static_hello_lib)
diff --git a/test cases/fortran/21 install static/subprojects/static_hello/static_hello.f90 b/test cases/fortran/21 install static/subprojects/static_hello/static_hello.f90
new file mode 100644
index 0000000..5407560
--- /dev/null
+++ b/test cases/fortran/21 install static/subprojects/static_hello/static_hello.f90
@@ -0,0 +1,17 @@
+module static_hello
+implicit none
+
+private
+public :: static_say_hello
+
+interface static_say_hello
+  module procedure say_hello
+end interface static_say_hello
+
+contains
+
+subroutine say_hello
+  print *, "Static library called."
+end subroutine say_hello
+
+end module static_hello
diff --git a/test cases/fortran/21 install static/test.json b/test cases/fortran/21 install static/test.json
new file mode 100644
index 0000000..b31e91d
--- /dev/null
+++ b/test cases/fortran/21 install static/test.json
@@ -0,0 +1,5 @@
+{
+    "installed": [
+        {"file": "usr/lib/libmainstatic.a", "type": "file"}
+    ]
+}
\ No newline at end of file
diff --git a/test cases/rust/11 generated main/gen.py b/test cases/rust/11 generated main/gen.py
index ebbc2a7..c8cfe76 100644
--- a/test cases/rust/11 generated main/gen.py
+++ b/test cases/rust/11 generated main/gen.py
@@ -6,10 +6,14 @@
 def main() -> None:
     parser = argparse.ArgumentParser()
     parser.add_argument('out')
+    parser.add_argument('--mode', choices=['main', 'lib'], default='main')
     args = parser.parse_args()
 
     with open(args.out, 'w') as f:
-        f.write('fn main() { println!("I prefer tarnish, actually.") }')
+        if args.mode == 'main':
+            f.write('fn main() { println!("I prefer tarnish, actually.") }')
+        elif args.mode == 'lib':
+            f.write('pub fn libfun() { println!("I prefer tarnish, actually.") }')
 
 
 if __name__ == "__main__":
diff --git a/test cases/rust/11 generated main/generated_lib_main.rs b/test cases/rust/11 generated main/generated_lib_main.rs
new file mode 100644
index 0000000..d9b373e
--- /dev/null
+++ b/test cases/rust/11 generated main/generated_lib_main.rs
@@ -0,0 +1,5 @@
+extern crate static_lib_generated as lib;
+
+fn main() {
+    lib::libfun();
+}
diff --git a/test cases/rust/11 generated main/meson.build b/test cases/rust/11 generated main/meson.build
index 4749816..695124c 100644
--- a/test cases/rust/11 generated main/meson.build
+++ b/test cases/rust/11 generated main/meson.build
@@ -11,6 +11,11 @@
 executable('custom_target_main', c)
 executable('custom_target_index_main', c[0])
 
-gen = generator(gen, arguments : ['@OUTPUT@'], output : '@BASENAME@.rs')
+gener = generator(gen, arguments : ['@OUTPUT@'], output : '@BASENAME@.rs')
 # Doesn't actually use gen.py as input, just a limitation of generators
-executable('generator_main', gen.process(['gen.py']))
+executable('generator_main', gener.process(['gen.py']))
+
+subdir('sub')
+executable('custom_target_subdir_main', s)
+
+executable('link_with_generated_lib', 'generated_lib_main.rs', link_with : lib)
diff --git a/test cases/rust/11 generated main/sub/meson.build b/test cases/rust/11 generated main/sub/meson.build
new file mode 100644
index 0000000..3ce96e5
--- /dev/null
+++ b/test cases/rust/11 generated main/sub/meson.build
@@ -0,0 +1,13 @@
+s = custom_target(
+  'subdir_target',
+  command : [gen, '@OUTPUT@'],
+  output : ['main.rs'],
+)
+
+l = custom_target(
+  'lib_target',
+  command : [gen, '@OUTPUT@', '--mode', 'lib'],
+  output : ['lib.rs'],
+)
+
+lib = static_library('static_lib_generated', l)