1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
|
# 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)
|