The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
322 lines
14 KiB
322 lines
14 KiB
# 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) |
|
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.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) |
|
_l = self.env.coredata.get_option(OptionKey('bindir')) |
|
assert isinstance(_l, str), 'for mypy' |
|
self.bindir = 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()), |
|
('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.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.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)
|
|
|