aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CODEOWNERS1
-rw-r--r--docs/markdown/External-Project-module.md116
-rw-r--r--docs/markdown/snippets/external_project.md24
-rw-r--r--docs/sitemap.txt1
-rw-r--r--mesonbuild/compilers/compilers.py3
-rw-r--r--mesonbuild/dependencies/base.py29
-rw-r--r--mesonbuild/envconfig.py1
-rw-r--r--mesonbuild/interpreter.py4
-rw-r--r--mesonbuild/mesonlib.py20
-rw-r--r--mesonbuild/modules/unstable_external_project.py253
-rw-r--r--mesonbuild/scripts/externalproject.py95
-rw-r--r--test cases/common/236 external project/app.c7
-rw-r--r--test cases/common/236 external project/func.c7
-rw-r--r--test cases/common/236 external project/func.h1
-rwxr-xr-xtest cases/common/236 external project/libfoo/configure44
-rw-r--r--test cases/common/236 external project/libfoo/libfoo.c8
-rw-r--r--test cases/common/236 external project/libfoo/libfoo.h3
-rw-r--r--test cases/common/236 external project/libfoo/meson.build22
-rw-r--r--test cases/common/236 external project/meson.build27
-rw-r--r--test cases/common/236 external project/test.json7
20 files changed, 654 insertions, 19 deletions
diff --git a/CODEOWNERS b/CODEOWNERS
index e361e83..48ce3ea 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,5 +1,6 @@
* @jpakkane
/mesonbuild/modules/pkgconfig.py @xclaesse
/mesonbuild/modules/cmake.py @mensinda
+/mesonbuild/modules/unstable_external_project.py @xclaesse
/mesonbuild/ast/* @mensinda
/mesonbuild/cmake/* @mensinda
diff --git a/docs/markdown/External-Project-module.md b/docs/markdown/External-Project-module.md
new file mode 100644
index 0000000..54b248f
--- /dev/null
+++ b/docs/markdown/External-Project-module.md
@@ -0,0 +1,116 @@
+# External Project module
+
+**Note**: the functionality of this module is governed by [Meson's
+ rules on mixing build systems](Mixing-build-systems.md).
+
+*This is an experimental module, API could change.*
+
+This module allows building code that uses build systems other than Meson. This
+module is intended to be used to build Autotools subprojects as fallback if the
+dependency couldn't be found on the system (e.g. too old distro version).
+
+The project will be compiled out-of-tree inside Meson's build directory. The
+project will also be installed inside Meson's build directory using make's
+[`DESTDIR`](https://www.gnu.org/prep/standards/html_node/DESTDIR.html)
+feature. During project installation step, that DESTDIR will be copied verbatim
+into the desired location.
+
+External subprojects can use libraries built by Meson (main project, or other
+subprojects) using pkg-config, thanks to `*-uninstalled.pc` files generated by
+[`pkg.generate()`](Pkgconfig-module.md).
+
+External build system requirements:
+- Must support out-of-tree build. The configure script will be invoked with the
+ current workdir inside Meson's build directory and not subproject's top source
+ directory.
+- Configure script must generate a `Makefile` in the current workdir.
+- Configure script must take common directories like prefix, libdir, etc, as
+ command line arguments.
+- Configure script must support common environment variable like CFLAGS, CC, etc.
+- Compilation step must detect when a reconfigure is needed, and do it
+ transparently.
+
+Known limitations:
+- Executables from external projects cannot be used uninstalled, because they
+ would need its libraries to be installed in the final location. This is why
+ there is no `find_program()` method.
+- The configure script must generate a `Makefile`, other build systems are not
+ yet supported.
+- When cross compiling, if `PKG_CONFIG_SYSROOT_DIR` is set in environment or
+ `sys_root` in the cross file properties, the external subproject will not be
+ able to find dependencies built by meson using pkg-config. The reason is
+ pkg-config and pkgconf both prepend the sysroot path to `-I` and `-L` arguments
+ from `-uninstalled.pc` files. This is arguably a bug that could be fixed in
+ future version of pkg-config/pkgconf.
+
+*Added 0.56.0*
+
+## Functions
+
+### `add_project()`
+
+This function should be called at the root directory of a project using another
+build system. Usually in a `meson.build` file placed in the top directory of a
+subproject, but could be also in any subdir.
+
+Its first positional argument is the name of the configure script to be
+executed (e.g. `configure` or `autogen.sh`), that file must be in the current
+directory and executable.
+
+Keyword arguments:
+- `configure_options`: An array of strings to be passed as arguments to the
+ configure script. Some special tags will be replaced by Meson before passing
+ them to the configure script: `@PREFIX@`, `@LIBDIR@` and `@INCLUDEDIR@`.
+ Note that `libdir` and `includedir` paths are relative to `prefix` in Meson
+ but some configure scripts requires absolute path, in that case they can be
+ passed as `'--libdir=@PREFIX@/@LIBDIR@'`.
+- `cross_configure_options`: Extra options appended to `configure_options` only
+ when cross compiling. special tag `@HOST@` will be replaced by
+ `'{}-{}-{}'.format(host_machine.cpu_family(), build_machine.system(), host_machine.system()`.
+ If omitted it defaults to `['--host=@HOST@']`.
+- `verbose`: If set to `true` the output of sub-commands ran to configure, build
+ and install the project will be printed onto Meson's stdout.
+- `env` : environment variables to set, such as `['NAME1=value1', 'NAME2=value2']`,
+ a dictionary, or an [`environment()` object](Reference-manual.md#environment-object).
+
+Returns an [`ExternalProject`](#ExternalProject_object) object
+
+## `ExternalProject` object
+
+### Methods
+
+#### `dependency(libname)`
+
+Return a dependency object that can be used to build targets against a library
+from the external project.
+
+Keyword arguments:
+- `subdir` path relative to `includedir` to be added to the header search path.
+
+## Example `meson.build` file for a subproject
+
+```meson
+project('My Autotools Project', 'c',
+ meson_version : '>=0.56.0',
+)
+
+mod = import('unstable_external_project')
+
+p = mod.add_project('configure',
+ configure_options : ['--prefix=@PREFIX@',
+ '--libdir=@LIBDIR@',
+ '--incdir=@INCLUDEDIR@',
+ '--enable-foo',
+ ],
+)
+
+mylib_dep = p.dependency('mylib')
+```
+
+## Using wrap file
+
+Most of the time the project will be built as a subproject, and fetched using
+a `.wrap` file. In that case the simple `meson.build` file needed to build the
+subproject can be provided by adding `patch_directory=mysubproject` line
+in the wrap file, and place the build definition file at
+`subprojects/packagefiles/mysubproject/meson.build`.
diff --git a/docs/markdown/snippets/external_project.md b/docs/markdown/snippets/external_project.md
new file mode 100644
index 0000000..0ecaac8
--- /dev/null
+++ b/docs/markdown/snippets/external_project.md
@@ -0,0 +1,24 @@
+## External projects
+
+A new experimental module `unstable_external_project` has been added to build
+code using other build systems than Meson. Currently only supporting projects
+with a configure script that generates Makefiles.
+
+```meson
+project('My Autotools Project', 'c',
+ meson_version : '>=0.56.0',
+)
+
+mod = import('unstable_external_project')
+
+p = mod.add_project('configure',
+ configure_options : ['--prefix=@PREFIX@',
+ '--libdir=@LIBDIR@',
+ '--incdir=@INCLUDEDIR@',
+ '--enable-foo',
+ ],
+)
+
+mylib_dep = p.dependency('mylib')
+```
+
diff --git a/docs/sitemap.txt b/docs/sitemap.txt
index ac74870..bdded3e 100644
--- a/docs/sitemap.txt
+++ b/docs/sitemap.txt
@@ -50,6 +50,7 @@ index.md
Windows-module.md
Cuda-module.md
Keyval-module.md
+ External-Project-module.md
Java.md
Vala.md
D.md
diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py
index c97bcc6..c4bd7c2 100644
--- a/mesonbuild/compilers/compilers.py
+++ b/mesonbuild/compilers/compilers.py
@@ -96,6 +96,9 @@ cflags_mapping = {'c': 'CFLAGS',
'vala': 'VALAFLAGS',
'rust': 'RUSTFLAGS'}
+cexe_mapping = {'c': 'CC',
+ 'cpp': 'CXX'}
+
# All these are only for C-linkable languages; see `clink_langs` above.
def sort_clink(lang):
diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py
index 69b81d5..8d2f759 100644
--- a/mesonbuild/dependencies/base.py
+++ b/mesonbuild/dependencies/base.py
@@ -642,28 +642,35 @@ class PkgConfigDependency(ExternalDependency):
mlog.debug("Called `{}` -> {}\n{}".format(call, rc, out))
return rc, out, err
- def _call_pkgbin(self, args, env=None):
- # Always copy the environment since we're going to modify it
- # with pkg-config variables
- if env is None:
- env = os.environ.copy()
- else:
- env = env.copy()
-
- extra_paths = self.env.coredata.builtins_per_machine[self.for_machine]['pkg_config_path'].value
- sysroot = self.env.properties[self.for_machine].get_sys_root()
+ @staticmethod
+ def setup_env(env, environment, for_machine, extra_path=None):
+ extra_paths = environment.coredata.builtins_per_machine[for_machine]['pkg_config_path'].value
+ if extra_path:
+ extra_paths.append(extra_path)
+ sysroot = environment.properties[for_machine].get_sys_root()
if sysroot:
env['PKG_CONFIG_SYSROOT_DIR'] = sysroot
new_pkg_config_path = ':'.join([p for p in extra_paths])
mlog.debug('PKG_CONFIG_PATH: ' + new_pkg_config_path)
env['PKG_CONFIG_PATH'] = new_pkg_config_path
- pkg_config_libdir_prop = self.env.properties[self.for_machine].get_pkg_config_libdir()
+ pkg_config_libdir_prop = environment.properties[for_machine].get_pkg_config_libdir()
if pkg_config_libdir_prop:
new_pkg_config_libdir = ':'.join([p for p in pkg_config_libdir_prop])
env['PKG_CONFIG_LIBDIR'] = new_pkg_config_libdir
mlog.debug('PKG_CONFIG_LIBDIR: ' + new_pkg_config_libdir)
+
+ def _call_pkgbin(self, args, env=None):
+ # Always copy the environment since we're going to modify it
+ # with pkg-config variables
+ if env is None:
+ env = os.environ.copy()
+ else:
+ env = env.copy()
+
+ PkgConfigDependency.setup_env(env, self.env, self.for_machine)
+
fenv = frozenset(env.items())
targs = tuple(args)
cache = PkgConfigDependency.pkgbin_cache
diff --git a/mesonbuild/envconfig.py b/mesonbuild/envconfig.py
index 3c562f3..6a813b8 100644
--- a/mesonbuild/envconfig.py
+++ b/mesonbuild/envconfig.py
@@ -339,6 +339,7 @@ class BinaryTable:
'cmake': 'CMAKE',
'qmake': 'QMAKE',
'pkgconfig': 'PKG_CONFIG',
+ 'make': 'MAKE',
} # type: T.Dict[str, str]
# Deprecated environment variables mapped from the new variable to the old one
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
index 99a2916..bc162ac 100644
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -2505,6 +2505,8 @@ class Interpreter(InterpreterBase):
return ExternalProgramHolder(item, self.subproject)
elif hasattr(item, 'held_object'):
return item
+ elif isinstance(item, InterpreterObject):
+ return item
else:
raise InterpreterException('Module returned a value of unknown type.')
@@ -2530,6 +2532,8 @@ class Interpreter(InterpreterBase):
# FIXME: This is special cased and not ideal:
# The first source is our new VapiTarget, the rest are deps
self.process_new_values(v.sources[0])
+ elif isinstance(v, InstallDir):
+ self.build.install_dirs.append(v)
elif hasattr(v, 'held_object'):
pass
elif isinstance(v, (int, str, bool, Disabler)):
diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py
index 943aa42..c6cbbd6 100644
--- a/mesonbuild/mesonlib.py
+++ b/mesonbuild/mesonlib.py
@@ -971,6 +971,17 @@ def do_define(regex: T.Pattern[str], line: str, confdata: 'ConfigurationData', v
else:
raise MesonException('#mesondefine argument "%s" is of unknown type.' % varname)
+def get_variable_regex(variable_format: str = 'meson') -> T.Pattern[str]:
+ # Only allow (a-z, A-Z, 0-9, _, -) as valid characters for a define
+ # Also allow escaping '@' with '\@'
+ if variable_format in ['meson', 'cmake@']:
+ regex = re.compile(r'(?:\\\\)+(?=\\?@)|\\@|@([-a-zA-Z0-9_]+)@')
+ elif variable_format == 'cmake':
+ regex = re.compile(r'(?:\\\\)+(?=\\?\$)|\\\${|\${([-a-zA-Z0-9_]+)}')
+ else:
+ raise MesonException('Format "{}" not handled'.format(variable_format))
+ return regex
+
def do_conf_str (data: list, confdata: 'ConfigurationData', variable_format: str,
encoding: str = 'utf-8') -> T.Tuple[T.List[str],T.Set[str], bool]:
def line_is_valid(line : str, variable_format: str) -> bool:
@@ -982,14 +993,7 @@ def do_conf_str (data: list, confdata: 'ConfigurationData', variable_format: str
return False
return True
- # Only allow (a-z, A-Z, 0-9, _, -) as valid characters for a define
- # Also allow escaping '@' with '\@'
- if variable_format in ['meson', 'cmake@']:
- regex = re.compile(r'(?:\\\\)+(?=\\?@)|\\@|@([-a-zA-Z0-9_]+)@')
- elif variable_format == 'cmake':
- regex = re.compile(r'(?:\\\\)+(?=\\?\$)|\\\${|\${([-a-zA-Z0-9_]+)}')
- else:
- raise MesonException('Format "{}" not handled'.format(variable_format))
+ regex = get_variable_regex(variable_format)
search_token = '#mesondefine'
if variable_format != 'meson':
diff --git a/mesonbuild/modules/unstable_external_project.py b/mesonbuild/modules/unstable_external_project.py
new file mode 100644
index 0000000..ff4685d
--- /dev/null
+++ b/mesonbuild/modules/unstable_external_project.py
@@ -0,0 +1,253 @@
+# Copyright 2020 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
+
+# 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.
+
+import os, subprocess, shlex
+from pathlib import Path
+
+from . import ExtensionModule, ModuleReturnValue
+from .. import mlog, build
+from ..mesonlib import (MesonException, Popen_safe, MachineChoice,
+ get_variable_regex, do_replacement)
+from ..interpreterbase import InterpreterObject, InterpreterException, FeatureNew
+from ..interpreterbase import stringArgs, permittedKwargs
+from ..interpreter import DependencyHolder, InstallDir
+from ..compilers.compilers import cflags_mapping, cexe_mapping
+from ..dependencies.base import InternalDependency, PkgConfigDependency
+
+class ExternalProject(InterpreterObject):
+ def __init__(self, interpreter, subdir, project_version, subproject, environment, build_machine, host_machine,
+ configure_command, configure_options, cross_configure_options, env, verbose):
+ InterpreterObject.__init__(self)
+ self.methods.update({'dependency': self.dependency_method,
+ })
+
+ self.interpreter = interpreter
+ self.subdir = Path(subdir)
+ self.project_version = project_version
+ self.subproject = subproject
+ self.env = environment
+ self.build_machine = build_machine
+ self.host_machine = host_machine
+ self.configure_command = configure_command
+ self.configure_options = configure_options
+ self.cross_configure_options = cross_configure_options
+ self.verbose = verbose
+ self.user_env = env
+
+ self.name = self.subdir.name
+ self.src_dir = Path(self.env.get_source_dir(), self.subdir)
+ self.build_dir = Path(self.env.get_build_dir(), self.subdir, 'build')
+ self.install_dir = Path(self.env.get_build_dir(), self.subdir, 'dist')
+ self.prefix = Path(self.env.coredata.get_builtin_option('prefix'))
+ self.libdir = Path(self.env.coredata.get_builtin_option('libdir'))
+ self.includedir = Path(self.env.coredata.get_builtin_option('includedir'))
+
+ # On Windows if the prefix is "c:/foo" and DESTDIR is "c:/bar", `make`
+ # will install files into "c:/bar/c:/foo" which is an invalid path.
+ # Work around that issue by removing the drive from prefix.
+ if self.prefix.drive:
+ self.prefix = self.prefix.relative_to(self.prefix.drive)
+
+ # self.prefix is an absolute path, so we cannot append it to another path.
+ self.rel_prefix = self.prefix.relative_to(self.prefix.root)
+
+ self.make = self.interpreter.find_program_impl('make')
+ self.make = self.make.get_command()[0]
+
+ self._configure()
+
+ self.targets = self._create_targets()
+
+ def _configure(self):
+ # Assume it's the name of a script in source dir, like 'configure',
+ # 'autogen.sh', etc).
+ configure_path = Path(self.src_dir, self.configure_command)
+ configure_prog = self.interpreter.find_program_impl(configure_path.as_posix())
+ configure_cmd = configure_prog.get_command()
+
+ d = {'PREFIX': self.prefix.as_posix(),
+ 'LIBDIR': self.libdir.as_posix(),
+ 'INCLUDEDIR': self.includedir.as_posix(),
+ }
+ self._validate_configure_options(d.keys())
+
+ configure_cmd += self._format_options(self.configure_options, d)
+
+ if self.env.is_cross_build():
+ host = '{}-{}-{}'.format(self.host_machine.cpu_family,
+ self.build_machine.system,
+ self.host_machine.system)
+ d = {'HOST': host}
+ configure_cmd += self._format_options(self.cross_configure_options, d)
+
+ # Set common env variables like CFLAGS, CC, etc.
+ link_exelist = []
+ link_args = []
+ self.run_env = os.environ.copy()
+ for lang, compiler in self.env.coredata.compilers[MachineChoice.HOST].items():
+ if any(lang not in i for i in (cexe_mapping, cflags_mapping)):
+ continue
+ cargs = self.env.coredata.get_external_args(MachineChoice.HOST, lang)
+ self.run_env[cexe_mapping[lang]] = self._quote_and_join(compiler.get_exelist())
+ self.run_env[cflags_mapping[lang]] = self._quote_and_join(cargs)
+ if not link_exelist:
+ link_exelist = compiler.get_linker_exelist()
+ link_args = self.env.coredata.get_external_link_args(MachineChoice.HOST, lang)
+ if link_exelist:
+ self.run_env['LD'] = self._quote_and_join(link_exelist)
+ self.run_env['LDFLAGS'] = self._quote_and_join(link_args)
+
+ self.run_env = self.user_env.get_env(self.run_env)
+
+ PkgConfigDependency.setup_env(self.run_env, self.env, MachineChoice.HOST,
+ Path(self.env.get_build_dir(), 'meson-uninstalled').as_posix())
+
+ self.build_dir.mkdir(parents=True, exist_ok=True)
+ self._run('configure', configure_cmd)
+
+ def _quote_and_join(self, array):
+ return ' '.join([shlex.quote(i) for i in array])
+
+ def _validate_configure_options(self, required_keys):
+ # Ensure the user at least try to pass basic info to the build system,
+ # like the prefix, libdir, etc.
+ for key in required_keys:
+ key_format = '@{}@'.format(key)
+ for option in self.configure_options:
+ if key_format in option:
+ break
+ else:
+ m = 'At least one configure option must contain "{}" key'
+ raise InterpreterException(m.format(key_format))
+
+ def _format_options(self, options, variables):
+ out = []
+ missing = set()
+ regex = get_variable_regex('meson')
+ confdata = {k: (v, None) for k, v in variables.items()}
+ for o in options:
+ arg, missing_vars = do_replacement(regex, o, 'meson', confdata)
+ missing.update(missing_vars)
+ out.append(arg)
+ if missing:
+ var_list = ", ".join(map(repr, sorted(missing)))
+ raise EnvironmentException(
+ "Variables {} in configure options are missing.".format(var_list))
+ return out
+
+ def _run(self, step, command):
+ mlog.log('External project {}:'.format(self.name), mlog.bold(step))
+ output = None if self.verbose else subprocess.DEVNULL
+ p, o, e = Popen_safe(command, cwd=str(self.build_dir), env=self.run_env,
+ stderr=subprocess.STDOUT,
+ stdout=output)
+ if p.returncode != 0:
+ m = '{} step failed:\n{}'.format(step, e)
+ raise MesonException(m)
+
+ def _create_targets(self):
+ cmd = self.env.get_build_command()
+ cmd += ['--internal', 'externalproject',
+ '--name', self.name,
+ '--srcdir', self.src_dir.as_posix(),
+ '--builddir', self.build_dir.as_posix(),
+ '--installdir', self.install_dir.as_posix(),
+ '--make', self.make,
+ ]
+ if self.verbose:
+ cmd.append('--verbose')
+
+ target_kwargs = {'output': '{}.stamp'.format(self.name),
+ 'depfile': '{}.d'.format(self.name),
+ 'command': cmd + ['@OUTPUT@', '@DEPFILE@'],
+ 'console': True,
+ }
+ self.target = build.CustomTarget(self.name,
+ self.subdir.as_posix(),
+ self.subproject,
+ target_kwargs)
+
+ idir = InstallDir(self.subdir.as_posix(),
+ Path('dist', self.rel_prefix).as_posix(),
+ install_dir='.',
+ install_mode=None,
+ exclude=None,
+ strip_directory=True,
+ from_source_dir=False)
+
+ return [self.target, idir]
+
+ @stringArgs
+ @permittedKwargs({'subdir'})
+ def dependency_method(self, args, kwargs):
+ if len(args) != 1:
+ m = 'ExternalProject.dependency takes exactly 1 positional arguments'
+ raise InterpreterException(m)
+ libname = args[0]
+
+ subdir = kwargs.get('subdir', '')
+ if not isinstance(subdir, str):
+ m = 'ExternalProject.dependency subdir keyword argument must be string.'
+ raise InterpreterException(m)
+
+ abs_includedir = Path(self.install_dir, self.rel_prefix, self.includedir)
+ if subdir:
+ abs_includedir = Path(abs_includedir, subdir)
+ abs_libdir = Path(self.install_dir, self.rel_prefix, self.libdir)
+
+ version = self.project_version['version']
+ incdir = []
+ compile_args = ['-I{}'.format(abs_includedir)]
+ link_args = ['-L{}'.format(abs_libdir), '-l{}'.format(libname)]
+ libs = []
+ libs_whole = []
+ sources = self.target
+ final_deps = []
+ variables = []
+ dep = InternalDependency(version, incdir, compile_args, link_args, libs,
+ libs_whole, sources, final_deps, variables)
+ return DependencyHolder(dep, self.subproject)
+
+
+class ExternalProjectModule(ExtensionModule):
+ @FeatureNew('External build system Module', '0.56.0')
+ def __init__(self, interpreter):
+ super().__init__(interpreter)
+
+ @stringArgs
+ @permittedKwargs({'configure_options', 'cross_configure_options', 'verbose', 'env'})
+ def add_project(self, state, args, kwargs):
+ if len(args) != 1:
+ raise InterpreterException('add_project takes exactly one positional argument')
+ configure_command = args[0]
+ configure_options = kwargs.get('configure_options', [])
+ cross_configure_options = kwargs.get('cross_configure_options', ['--host={host}'])
+ verbose = kwargs.get('verbose', False)
+ env = self.interpreter.unpack_env_kwarg(kwargs)
+ project = ExternalProject(self.interpreter,
+ state.subdir,
+ state.project_version,
+ state.subproject,
+ state.environment,
+ state.build_machine,
+ state.host_machine,
+ configure_command,
+ configure_options,
+ cross_configure_options,
+ env, verbose)
+ return ModuleReturnValue(project, project.targets)
+
+
+def initialize(*args, **kwargs):
+ return ExternalProjectModule(*args, **kwargs)
diff --git a/mesonbuild/scripts/externalproject.py b/mesonbuild/scripts/externalproject.py
new file mode 100644
index 0000000..6c3a89c
--- /dev/null
+++ b/mesonbuild/scripts/externalproject.py
@@ -0,0 +1,95 @@
+# Copyright 2019 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
+
+# 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.
+
+import os
+import argparse
+import multiprocessing
+import subprocess
+from pathlib import Path
+
+from ..mesonlib import Popen_safe
+
+class ExternalProject:
+ def __init__(self, options):
+ self.name = options.name
+ self.src_dir = options.srcdir
+ self.build_dir = options.builddir
+ self.install_dir = options.installdir
+ self.verbose = options.verbose
+ self.stampfile = options.stampfile
+ self.depfile = options.depfile
+ self.make = options.make
+
+ def write_depfile(self):
+ with open(self.depfile, 'w') as f:
+ f.write('{}: \\\n'.format(self.stampfile))
+ for dirpath, dirnames, filenames in os.walk(self.src_dir):
+ dirnames[:] = [d for d in dirnames if not d.startswith('.')]
+ for fname in filenames:
+ if fname.startswith('.'):
+ continue
+ path = Path(dirpath, fname)
+ f.write(' {} \\\n'.format(path.as_posix().replace(' ', '\\ ')))
+
+ def write_stampfile(self):
+ with open(self.stampfile, 'w') as f:
+ pass
+
+ def gnu_make(self):
+ p, o, e = Popen_safe([self.make, '--version'])
+ if p.returncode == 0 and 'GNU Make' in o:
+ return True
+ return False
+
+ def build(self):
+ make_cmd = [self.make]
+ if not self.verbose:
+ make_cmd.append('--quiet')
+ if self.gnu_make():
+ make_cmd.append('-j' + str(multiprocessing.cpu_count()))
+
+ rc = self._run(make_cmd)
+ if rc != 0:
+ return rc
+
+ install_cmd = make_cmd + ['DESTDIR= ' + self.install_dir, 'install']
+ rc = self._run(install_cmd)
+ if rc != 0:
+ return rc
+
+ self.write_depfile()
+ self.write_stampfile()
+
+ return 0
+
+ def _run(self, command):
+ output = None if self.verbose else subprocess.DEVNULL
+ p, o, e = Popen_safe(command, stderr=subprocess.STDOUT, stdout=output,
+ cwd=self.build_dir)
+ return p.returncode
+
+def run(args):
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--name')
+ parser.add_argument('--srcdir')
+ parser.add_argument('--builddir')
+ parser.add_argument('--installdir')
+ parser.add_argument('--make')
+ parser.add_argument('--verbose', action='store_true')
+ parser.add_argument('stampfile')
+ parser.add_argument('depfile')
+
+ options = parser.parse_args(args)
+ ep = ExternalProject(options)
+ return ep.build()
diff --git a/test cases/common/236 external project/app.c b/test cases/common/236 external project/app.c
new file mode 100644
index 0000000..166f007
--- /dev/null
+++ b/test cases/common/236 external project/app.c
@@ -0,0 +1,7 @@
+#include <libfoo.h>
+
+int main(void)
+{
+ return call_foo() == 42 ? 0 : 1;
+}
+
diff --git a/test cases/common/236 external project/func.c b/test cases/common/236 external project/func.c
new file mode 100644
index 0000000..5e8f933
--- /dev/null
+++ b/test cases/common/236 external project/func.c
@@ -0,0 +1,7 @@
+#include "func.h"
+
+int func(void)
+{
+ return 1;
+}
+
diff --git a/test cases/common/236 external project/func.h b/test cases/common/236 external project/func.h
new file mode 100644
index 0000000..340b82a
--- /dev/null
+++ b/test cases/common/236 external project/func.h
@@ -0,0 +1 @@
+int func(void);
diff --git a/test cases/common/236 external project/libfoo/configure b/test cases/common/236 external project/libfoo/configure
new file mode 100755
index 0000000..a867b48
--- /dev/null
+++ b/test cases/common/236 external project/libfoo/configure
@@ -0,0 +1,44 @@
+#! /bin/sh
+
+srcdir=$(dirname "$0")
+
+for i in "$@"
+do
+case $i in
+ --prefix=*)
+ PREFIX="${i#*=}"
+ shift
+ ;;
+ --libdir=*)
+ LIBDIR="${i#*=}"
+ shift
+ ;;
+ --includedir=*)
+ INCDIR="${i#*=}"
+ shift
+ ;;
+ --libext=*)
+ LIBEXT="${i#*=}"
+ shift
+ ;;
+ *)
+ shift
+ ;;
+esac
+done
+
+DEP_ARGS=$(pkg-config somelib --cflags --libs)
+
+cat > Makefile << EOL
+all: libfoo.$LIBEXT
+
+libfoo.$LIBEXT:
+ $CC "$srcdir/libfoo.c" -shared -fPIC $DEP_ARGS -o \$@
+
+install: libfoo.$LIBEXT
+ mkdir -p "\$(DESTDIR)$LIBDIR";
+ mkdir -p "\$(DESTDIR)$LIBDIR/pkgconfig";
+ mkdir -p "\$(DESTDIR)$INCDIR";
+ cp \$< "\$(DESTDIR)$LIBDIR";
+ cp "$srcdir/libfoo.h" "\$(DESTDIR)$INCDIR";
+EOL
diff --git a/test cases/common/236 external project/libfoo/libfoo.c b/test cases/common/236 external project/libfoo/libfoo.c
new file mode 100644
index 0000000..3f62282
--- /dev/null
+++ b/test cases/common/236 external project/libfoo/libfoo.c
@@ -0,0 +1,8 @@
+#include "libfoo.h"
+
+int func(void);
+
+int call_foo()
+{
+ return func() == 1 ? 42 : 0;
+}
diff --git a/test cases/common/236 external project/libfoo/libfoo.h b/test cases/common/236 external project/libfoo/libfoo.h
new file mode 100644
index 0000000..8981f18
--- /dev/null
+++ b/test cases/common/236 external project/libfoo/libfoo.h
@@ -0,0 +1,3 @@
+#pragma once
+
+int call_foo(void);
diff --git a/test cases/common/236 external project/libfoo/meson.build b/test cases/common/236 external project/libfoo/meson.build
new file mode 100644
index 0000000..941e13f
--- /dev/null
+++ b/test cases/common/236 external project/libfoo/meson.build
@@ -0,0 +1,22 @@
+mod = import('unstable_external_project')
+
+target_system = target_machine.system()
+if target_system in ['windows', 'cygwin']
+ libext = 'dll'
+elif target_system == 'darwin'
+ libext = 'dylib'
+else
+ libext = 'so'
+endif
+
+p = mod.add_project('configure',
+ configure_options : [
+ '--prefix=@PREFIX@',
+ '--libdir=@PREFIX@/@LIBDIR@',
+ '--includedir=@PREFIX@/@INCLUDEDIR@',
+ '--libext=' + libext,
+ ],
+)
+
+libfoo_dep = declare_dependency(link_with : somelib,
+ dependencies : p.dependency('foo'))
diff --git a/test cases/common/236 external project/meson.build b/test cases/common/236 external project/meson.build
new file mode 100644
index 0000000..d1ed797
--- /dev/null
+++ b/test cases/common/236 external project/meson.build
@@ -0,0 +1,27 @@
+project('test external project', 'c')
+
+if not find_program('pkg-config', required: false).found()
+ error('MESON_SKIP_TEST: pkg-config not found')
+endif
+
+if not find_program('make', required : false).found()
+ error('MESON_SKIP_TEST: make not found')
+endif
+
+if host_machine.system() == 'windows'
+ error('MESON_SKIP_TEST: The fake configure script is too dumb to work on Windows')
+endif
+
+if meson.is_cross_build()
+ # CI uses PKG_CONFIG_SYSROOT_DIR which breaks -uninstalled.pc usage.
+ error('MESON_SKIP_TEST: Cross build support is too limited for this test')
+endif
+
+pkg = import('pkgconfig')
+
+somelib = library('somelib', 'func.c')
+pkg.generate(somelib)
+
+subdir('libfoo')
+
+executable('test-find-library', 'app.c', dependencies : libfoo_dep)
diff --git a/test cases/common/236 external project/test.json b/test cases/common/236 external project/test.json
new file mode 100644
index 0000000..4888e87
--- /dev/null
+++ b/test cases/common/236 external project/test.json
@@ -0,0 +1,7 @@
+{
+ "installed": [
+ { "type": "shared_lib", "file": "usr/lib/foo" },
+ { "type": "file", "file": "usr/include/libfoo.h" },
+ { "type": "file", "file": "usr/lib/pkgconfig/somelib.pc" }
+ ]
+}