aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/modules/external_project.py
diff options
context:
space:
mode:
Diffstat (limited to 'mesonbuild/modules/external_project.py')
-rw-r--r--mesonbuild/modules/external_project.py306
1 files changed, 306 insertions, 0 deletions
diff --git a/mesonbuild/modules/external_project.py b/mesonbuild/modules/external_project.py
new file mode 100644
index 0000000..1fd4911
--- /dev/null
+++ b/mesonbuild/modules/external_project.py
@@ -0,0 +1,306 @@
+# 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.
+
+from pathlib import Path
+import os
+import shlex
+import subprocess
+import typing as T
+
+from . import ExtensionModule, ModuleReturnValue, NewExtensionModule, ModuleInfo
+from .. import mlog, build
+from ..compilers.compilers import CFLAGS_MAPPING
+from ..envconfig import ENV_VAR_PROG_MAP
+from ..dependencies import InternalDependency, PkgConfigDependency
+from ..interpreterbase import FeatureNew
+from ..interpreter.type_checking import ENV_KW, DEPENDS_KW
+from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args
+from ..mesonlib import (EnvironmentException, MesonException, Popen_safe, MachineChoice,
+ get_variable_regex, do_replacement, join_args, OptionKey)
+
+if T.TYPE_CHECKING:
+ from typing_extensions import TypedDict
+
+ from . import ModuleState
+ from ..interpreter import Interpreter
+ from ..interpreterbase import TYPE_var
+ from ..build import BuildTarget, CustomTarget
+
+ class Dependency(TypedDict):
+
+ subdir: str
+
+ class AddProject(TypedDict):
+
+ configure_options: T.List[str]
+ cross_configure_options: T.List[str]
+ verbose: bool
+ env: build.EnvironmentVariables
+ depends: T.List[T.Union[BuildTarget, CustomTarget]]
+
+
+class ExternalProject(NewExtensionModule):
+ def __init__(self,
+ state: 'ModuleState',
+ configure_command: str,
+ configure_options: T.List[str],
+ cross_configure_options: T.List[str],
+ env: build.EnvironmentVariables,
+ verbose: bool,
+ extra_depends: T.List[T.Union['BuildTarget', 'CustomTarget']]):
+ super().__init__()
+ self.methods.update({'dependency': self.dependency_method,
+ })
+
+ self.subdir = Path(state.subdir)
+ self.project_version = state.project_version
+ self.subproject = state.subproject
+ self.env = state.environment
+ self.build_machine = state.build_machine
+ self.host_machine = state.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.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')
+ _p = self.env.coredata.get_option(OptionKey('prefix'))
+ assert isinstance(_p, str), 'for mypy'
+ self.prefix = Path(_p)
+ _l = self.env.coredata.get_option(OptionKey('libdir'))
+ assert isinstance(_l, str), 'for mypy'
+ self.libdir = Path(_l)
+ _i = self.env.coredata.get_option(OptionKey('includedir'))
+ assert isinstance(_i, str), 'for mypy'
+ self.includedir = Path(_i)
+ self.name = self.src_dir.name
+
+ # 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._configure(state)
+
+ self.targets = self._create_targets(extra_depends)
+
+ def _configure(self, state: 'ModuleState') -> None:
+ if self.configure_command == 'waf':
+ FeatureNew('Waf external project', '0.60.0').use(self.subproject, state.current_node)
+ waf = state.find_program('waf')
+ configure_cmd = waf.get_command()
+ configure_cmd += ['configure', '-o', str(self.build_dir)]
+ workdir = self.src_dir
+ self.make = waf.get_command() + ['build']
+ else:
+ # 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 = state.find_program(configure_path.as_posix())
+ configure_cmd = configure_prog.get_command()
+ workdir = self.build_dir
+ self.make = state.find_program('make').get_command()
+
+ d = [('PREFIX', '--prefix=@PREFIX@', self.prefix.as_posix()),
+ ('LIBDIR', '--libdir=@PREFIX@/@LIBDIR@', self.libdir.as_posix()),
+ ('INCLUDEDIR', None, self.includedir.as_posix()),
+ ]
+ self._validate_configure_options(d, state)
+
+ 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', None, host)]
+ configure_cmd += self._format_options(self.cross_configure_options, d)
+
+ # Set common env variables like CFLAGS, CC, etc.
+ link_exelist: T.List[str] = []
+ link_args: T.List[str] = []
+ 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 (ENV_VAR_PROG_MAP, CFLAGS_MAPPING)):
+ continue
+ cargs = self.env.coredata.get_external_args(MachineChoice.HOST, lang)
+ assert isinstance(cargs, list), 'for mypy'
+ self.run_env[ENV_VAR_PROG_MAP[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()
+ _l = self.env.coredata.get_external_link_args(MachineChoice.HOST, lang)
+ assert isinstance(_l, list), 'for mypy'
+ link_args = _l
+ if link_exelist:
+ # FIXME: Do not pass linker because Meson uses CC as linker wrapper,
+ # but autotools often expects the real linker (e.h. GNU ld).
+ # self.run_env['LD'] = self._quote_and_join(link_exelist)
+ pass
+ self.run_env['LDFLAGS'] = self._quote_and_join(link_args)
+
+ self.run_env = self.user_env.get_env(self.run_env)
+ self.run_env = PkgConfigDependency.setup_env(self.run_env, self.env, MachineChoice.HOST,
+ uninstalled=True)
+
+ self.build_dir.mkdir(parents=True, exist_ok=True)
+ self._run('configure', configure_cmd, workdir)
+
+ def _quote_and_join(self, array: T.List[str]) -> str:
+ return ' '.join([shlex.quote(i) for i in array])
+
+ def _validate_configure_options(self, variables: T.List[T.Tuple[str, str, str]], state: 'ModuleState') -> None:
+ # Ensure the user at least try to pass basic info to the build system,
+ # like the prefix, libdir, etc.
+ for key, default, val in variables:
+ if default is None:
+ continue
+ key_format = f'@{key}@'
+ for option in self.configure_options:
+ if key_format in option:
+ break
+ else:
+ FeatureNew('Default configure_option', '0.57.0').use(self.subproject, state.current_node)
+ self.configure_options.append(default)
+
+ def _format_options(self, options: T.List[str], variables: T.List[T.Tuple[str, str, str]]) -> T.List[str]:
+ out: T.List[str] = []
+ missing = set()
+ regex = get_variable_regex('meson')
+ confdata: T.Dict[str, T.Tuple[str, T.Optional[str]]] = {k: (v, None) for k, _, v in variables}
+ 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(
+ f"Variables {var_list} in configure options are missing.")
+ return out
+
+ def _run(self, step: str, command: T.List[str], workdir: Path) -> None:
+ mlog.log(f'External project {self.name}:', mlog.bold(step))
+ m = 'Running command ' + str(command) + ' in directory ' + str(workdir) + '\n'
+ log_filename = Path(mlog.log_dir, f'{self.name}-{step}.log')
+ output = None
+ if not self.verbose:
+ output = open(log_filename, 'w', encoding='utf-8')
+ output.write(m + '\n')
+ output.flush()
+ else:
+ mlog.log(m)
+ p, *_ = Popen_safe(command, cwd=workdir, env=self.run_env,
+ stderr=subprocess.STDOUT,
+ stdout=output)
+ if p.returncode != 0:
+ m = f'{step} step returned error code {p.returncode}.'
+ if not self.verbose:
+ m += '\nSee logs: ' + str(log_filename)
+ raise MesonException(m)
+
+ def _create_targets(self, extra_depends: T.List[T.Union['BuildTarget', 'CustomTarget']]) -> T.List['TYPE_var']:
+ 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(),
+ '--logdir', mlog.log_dir,
+ '--make', join_args(self.make),
+ ]
+ if self.verbose:
+ cmd.append('--verbose')
+
+ self.target = build.CustomTarget(
+ self.name,
+ self.subdir.as_posix(),
+ self.subproject,
+ self.env,
+ cmd + ['@OUTPUT@', '@DEPFILE@'],
+ [],
+ [f'{self.name}.stamp'],
+ depfile=f'{self.name}.d',
+ console=True,
+ extra_depends=extra_depends,
+ )
+
+ idir = build.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,
+ subproject=self.subproject)
+
+ return [self.target, idir]
+
+ @typed_pos_args('external_project.dependency', str)
+ @typed_kwargs('external_project.dependency', KwargInfo('subdir', str, default=''))
+ def dependency_method(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'Dependency') -> InternalDependency:
+ libname = args[0]
+
+ abs_includedir = Path(self.install_dir, self.rel_prefix, self.includedir)
+ if kwargs['subdir']:
+ abs_includedir = Path(abs_includedir, kwargs['subdir'])
+ abs_libdir = Path(self.install_dir, self.rel_prefix, self.libdir)
+
+ version = self.project_version
+ compile_args = [f'-I{abs_includedir}']
+ link_args = [f'-L{abs_libdir}', f'-l{libname}']
+ sources = self.target
+ dep = InternalDependency(version, [], compile_args, link_args, [],
+ [], [sources], [], {}, [], [])
+ return dep
+
+
+class ExternalProjectModule(ExtensionModule):
+
+ INFO = ModuleInfo('External build system', '0.56.0', unstable=True)
+
+ def __init__(self, interpreter: 'Interpreter'):
+ super().__init__(interpreter)
+ self.methods.update({'add_project': self.add_project,
+ })
+
+ @typed_pos_args('external_project_mod.add_project', str)
+ @typed_kwargs(
+ 'external_project.add_project',
+ KwargInfo('configure_options', ContainerTypeInfo(list, str), default=[], listify=True),
+ KwargInfo('cross_configure_options', ContainerTypeInfo(list, str), default=['--host=@HOST@'], listify=True),
+ KwargInfo('verbose', bool, default=False),
+ ENV_KW,
+ DEPENDS_KW.evolve(since='0.63.0'),
+ )
+ def add_project(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'AddProject') -> ModuleReturnValue:
+ configure_command = args[0]
+ project = ExternalProject(state,
+ configure_command,
+ kwargs['configure_options'],
+ kwargs['cross_configure_options'],
+ kwargs['env'],
+ kwargs['verbose'],
+ kwargs['depends'])
+ return ModuleReturnValue(project, project.targets)
+
+
+def initialize(interp: 'Interpreter') -> ExternalProjectModule:
+ return ExternalProjectModule(interp)