# 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, 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.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(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.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(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' log_filename = Path(mlog.get_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.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.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)