| # SPDX-License-Identifier: Apache-2.0 | |
| # Copyright 2020 The Meson development team | |
| from __future__ import annotations | |
| 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 | |
| from ..dependencies.pkgconfig import PkgConfigInterface | |
| 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, relpath) | |
| from ..options import OptionKey | |
| if T.TYPE_CHECKING: | |
| from typing_extensions import TypedDict | |
| from . import ModuleState | |
| from .._typing import ImmutableListProtocol | |
| from ..build import BuildTarget, CustomTarget | |
| from ..interpreter import Interpreter | |
| from ..interpreterbase import TYPE_var | |
| from ..mesonlib import EnvironmentVariables | |
| from ..utils.core import EnvironOrDict | |
| class Dependency(TypedDict): | |
| subdir: str | |
| class AddProject(TypedDict): | |
| configure_options: T.List[str] | |
| cross_configure_options: T.List[str] | |
| verbose: bool | |
| env: EnvironmentVariables | |
| depends: T.List[T.Union[BuildTarget, CustomTarget]] | |
| class ExternalProject(NewExtensionModule): | |
| make: ImmutableListProtocol[str] | |
| def __init__(self, | |
| state: 'ModuleState', | |
| configure_command: str, | |
| configure_options: T.List[str], | |
| cross_configure_options: T.List[str], | |
| env: 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.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.optstore.get_value_for(OptionKey('prefix')) | |
| assert isinstance(_p, str), 'for mypy' | |
| self.prefix = Path(_p) | |
| _l = self.env.coredata.optstore.get_value_for(OptionKey('libdir')) | |
| assert isinstance(_l, str), 'for mypy' | |
| self.libdir = Path(_l) | |
| _l = self.env.coredata.optstore.get_value_for(OptionKey('bindir')) | |
| assert isinstance(_l, str), 'for mypy' | |
| self.bindir = Path(_l) | |
| _i = self.env.coredata.optstore.get_value_for(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 = Path(relpath(self.prefix, self.prefix.drive)) | |
| # self.prefix is an absolute path, so we cannot append it to another path. | |
| self.rel_prefix = Path(relpath(self.prefix, 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()), | |
| ('BINDIR', '--bindir=@PREFIX@/@BINDIR@', self.bindir.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(state.environment.machines.host.cpu, | |
| 'pc' if state.environment.machines.host.cpu_family in {"x86", "x86_64"} | |
| else 'unknown', | |
| state.environment.machines.host.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: EnvironOrDict = 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 = PkgConfigInterface.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.Sequence[T.Tuple[str, T.Optional[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.Sequence[T.Tuple[str, T.Optional[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(repr(m) for m in 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' | |
| logfile = Path(mlog.get_log_dir(), f'{self.name}-{step}.log') | |
| output = None | |
| if not self.verbose: | |
| output = open(logfile, '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(logfile) | |
| contents = mlog.ci_fold_file(logfile, f'CI platform detected, click here for {os.path.basename(logfile)} contents.') | |
| if contents: | |
| print(contents) | |
| 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.get_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, | |
| description='Generating external project {}', | |
| ) | |
| idir = build.InstallDir(self.subdir.as_posix(), | |
| Path('dist', self.rel_prefix).as_posix(), | |
| install_dir='.', | |
| install_dir_name='.', | |
| 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.devenv: T.Optional[EnvironmentVariables] = None | |
| 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']) | |
| abs_libdir = Path(project.install_dir, project.rel_prefix, project.libdir).as_posix() | |
| abs_bindir = Path(project.install_dir, project.rel_prefix, project.bindir).as_posix() | |
| env = state.environment.get_env_for_paths({abs_libdir}, {abs_bindir}) | |
| if self.devenv is None: | |
| self.devenv = env | |
| else: | |
| self.devenv.merge(env) | |
| return ModuleReturnValue(project, project.targets) | |
| def postconf_hook(self, b: build.Build) -> None: | |
| if self.devenv is not None: | |
| b.devenv.append(self.devenv) | |
| def initialize(interp: 'Interpreter') -> ExternalProjectModule: | |
| return ExternalProjectModule(interp) |