diff options
author | Xavier Claessens <xavier.claessens@collabora.com> | 2018-10-04 00:07:44 -0400 |
---|---|---|
committer | Xavier Claessens <xclaesse@gmail.com> | 2020-09-13 13:54:47 -0400 |
commit | 9d338200dacdf24c50259c309380200f8a53d5b5 (patch) | |
tree | 8e268a9357119265f11b30791f56e8e09fec393e /mesonbuild/modules | |
parent | 19696c3dcdb379f4c5b88457f43242f2d33427a0 (diff) | |
download | meson-9d338200dacdf24c50259c309380200f8a53d5b5.zip meson-9d338200dacdf24c50259c309380200f8a53d5b5.tar.gz meson-9d338200dacdf24c50259c309380200f8a53d5b5.tar.bz2 |
external-project: New module to build configure/make projects
This adds an experimental meson module to build projects with other
build systems.
Closes: #4316
Diffstat (limited to 'mesonbuild/modules')
-rw-r--r-- | mesonbuild/modules/unstable_external_project.py | 253 |
1 files changed, 253 insertions, 0 deletions
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) |