|
|
|
# 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.
|
|
|
|
|
|
|
|
import os, subprocess, shlex
|
|
|
|
from pathlib import Path
|
|
|
|
import typing as T
|
|
|
|
|
|
|
|
from . import ExtensionModule, ModuleReturnValue
|
|
|
|
from .. import mlog, build
|
|
|
|
from ..mesonlib import (MesonException, Popen_safe, MachineChoice,
|
|
|
|
get_variable_regex, do_replacement)
|
|
|
|
from ..interpreterbase import InterpreterObject, InterpreterException, FeatureNew
|
|
|
|
from ..interpreterbase import stringArgs, permittedKwargs
|
|
|
|
from ..interpreter import Interpreter, DependencyHolder
|
|
|
|
from ..compilers.compilers import CFLAGS_MAPPING, CEXE_MAPPING
|
|
|
|
from ..dependencies.base import InternalDependency, PkgConfigDependency
|
|
|
|
from ..environment import Environment
|
|
|
|
from ..mesonlib import OptionKey
|
|
|
|
|
|
|
|
class ExternalProject(InterpreterObject):
|
|
|
|
def __init__(self,
|
|
|
|
interpreter: Interpreter,
|
|
|
|
subdir: str,
|
|
|
|
project_version: T.Dict[str, str],
|
|
|
|
subproject: str,
|
|
|
|
environment: Environment,
|
|
|
|
build_machine: str,
|
|
|
|
host_machine: str,
|
|
|
|
configure_command: T.List[str],
|
|
|
|
configure_options: T.List[str],
|
|
|
|
cross_configure_options: T.List[str],
|
|
|
|
env: build.EnvironmentVariables,
|
|
|
|
verbose: bool):
|
|
|
|
InterpreterObject.__init__(self)
|
|
|
|
self.methods.update({'dependency': self.dependency_method,
|
|
|
|
})
|
|
|
|
|
|
|
|
self.interpreter = interpreter
|
|
|
|
self.subdir = Path(subdir)
|
|
|
|
self.project_version = project_version
|
|
|
|
self.subproject = subproject
|
|
|
|
self.env = environment
|
|
|
|
self.build_machine = build_machine
|
|
|
|
self.host_machine = 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.name = self.subdir.name
|
|
|
|
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')
|
|
|
|
self.prefix = Path(self.env.coredata.get_option(OptionKey('prefix')))
|
|
|
|
self.libdir = Path(self.env.coredata.get_option(OptionKey('libdir')))
|
|
|
|
self.includedir = Path(self.env.coredata.get_option(OptionKey('includedir')))
|
|
|
|
|
|
|
|
# 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.make = self.interpreter.find_program_impl('make')
|
|
|
|
self.make = self.make.get_command()[0]
|
|
|
|
|
|
|
|
self._configure()
|
|
|
|
|
|
|
|
self.targets = self._create_targets()
|
|
|
|
|
|
|
|
def _configure(self):
|
|
|
|
# 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 = self.interpreter.find_program_impl(configure_path.as_posix())
|
|
|
|
configure_cmd = configure_prog.get_command()
|
|
|
|
|
|
|
|
d = [('PREFIX', '--prefix=@PREFIX@', self.prefix.as_posix()),
|
|
|
|
('LIBDIR', '--libdir=@PREFIX@/@LIBDIR@', self.libdir.as_posix()),
|
|
|
|
('INCLUDEDIR', '--includedir=@PREFIX@/@INCLUDEDIR@', self.includedir.as_posix()),
|
|
|
|
]
|
|
|
|
self._validate_configure_options(d)
|
|
|
|
|
|
|
|
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 = []
|
|
|
|
link_args = []
|
|
|
|
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 (CEXE_MAPPING, CFLAGS_MAPPING)):
|
|
|
|
continue
|
|
|
|
cargs = self.env.coredata.get_external_args(MachineChoice.HOST, lang)
|
|
|
|
self.run_env[CEXE_MAPPING[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()
|
|
|
|
link_args = self.env.coredata.get_external_link_args(MachineChoice.HOST, lang)
|
|
|
|
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)
|
|
|
|
|
|
|
|
PkgConfigDependency.setup_env(self.run_env, self.env, MachineChoice.HOST,
|
|
|
|
Path(self.env.get_build_dir(), 'meson-uninstalled').as_posix())
|
|
|
|
|
|
|
|
self.build_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self._run('configure', configure_cmd)
|
|
|
|
|
|
|
|
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]]):
|
|
|
|
# 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:
|
|
|
|
key_format = '@{}@'.format(key)
|
|
|
|
for option in self.configure_options:
|
|
|
|
if key_format in option:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
FeatureNew('Default configure_option', '0.57.0').use(self.subproject)
|
|
|
|
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 = []
|
|
|
|
missing = set()
|
|
|
|
regex = get_variable_regex('meson')
|
|
|
|
confdata = {k: (v, None) for k, d, 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(
|
|
|
|
"Variables {} in configure options are missing.".format(var_list))
|
|
|
|
return out
|
|
|
|
|
|
|
|
def _run(self, step: str, command: T.List[str]):
|
|
|
|
mlog.log('External project {}:'.format(self.name), mlog.bold(step))
|
|
|
|
m = 'Running command ' + str(command) + ' in directory ' + str(self.build_dir) + '\n'
|
|
|
|
log_filename = Path(mlog.log_dir, '{}-{}.log'.format(self.name, step))
|
|
|
|
output = None
|
|
|
|
if not self.verbose:
|
|
|
|
output = open(log_filename, 'w')
|
|
|
|
output.write(m + '\n')
|
|
|
|
output.flush()
|
|
|
|
else:
|
|
|
|
mlog.log(m)
|
|
|
|
p, o, e = Popen_safe(command, cwd=str(self.build_dir), env=self.run_env,
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
stdout=output)
|
|
|
|
if p.returncode != 0:
|
|
|
|
m = '{} step returned error code {}.'.format(step, p.returncode)
|
|
|
|
if not self.verbose:
|
|
|
|
m += '\nSee logs: ' + str(log_filename)
|
|
|
|
raise MesonException(m)
|
|
|
|
|
|
|
|
def _create_targets(self):
|
|
|
|
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', self.make,
|
|
|
|
]
|
|
|
|
if self.verbose:
|
|
|
|
cmd.append('--verbose')
|
|
|
|
|
|
|
|
target_kwargs = {'output': '{}.stamp'.format(self.name),
|
|
|
|
'depfile': '{}.d'.format(self.name),
|
|
|
|
'command': cmd + ['@OUTPUT@', '@DEPFILE@'],
|
|
|
|
'console': True,
|
|
|
|
}
|
|
|
|
self.target = build.CustomTarget(self.name,
|
|
|
|
self.subdir.as_posix(),
|
|
|
|
self.subproject,
|
|
|
|
target_kwargs)
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
@stringArgs
|
|
|
|
@permittedKwargs({'subdir'})
|
|
|
|
def dependency_method(self, args, kwargs):
|
|
|
|
if len(args) != 1:
|
|
|
|
m = 'ExternalProject.dependency takes exactly 1 positional arguments'
|
|
|
|
raise InterpreterException(m)
|
|
|
|
libname = args[0]
|
|
|
|
|
|
|
|
subdir = kwargs.get('subdir', '')
|
|
|
|
if not isinstance(subdir, str):
|
|
|
|
m = 'ExternalProject.dependency subdir keyword argument must be string.'
|
|
|
|
raise InterpreterException(m)
|
|
|
|
|
|
|
|
abs_includedir = Path(self.install_dir, self.rel_prefix, self.includedir)
|
|
|
|
if subdir:
|
|
|
|
abs_includedir = Path(abs_includedir, subdir)
|
|
|
|
abs_libdir = Path(self.install_dir, self.rel_prefix, self.libdir)
|
|
|
|
|
|
|
|
version = self.project_version['version']
|
|
|
|
incdir = []
|
|
|
|
compile_args = ['-I{}'.format(abs_includedir)]
|
|
|
|
link_args = ['-L{}'.format(abs_libdir), '-l{}'.format(libname)]
|
|
|
|
libs = []
|
|
|
|
libs_whole = []
|
|
|
|
sources = self.target
|
|
|
|
final_deps = []
|
|
|
|
variables = []
|
|
|
|
dep = InternalDependency(version, incdir, compile_args, link_args, libs,
|
|
|
|
libs_whole, sources, final_deps, variables)
|
|
|
|
return DependencyHolder(dep, self.subproject)
|
|
|
|
|
|
|
|
|
|
|
|
class ExternalProjectModule(ExtensionModule):
|
|
|
|
@FeatureNew('External build system Module', '0.56.0')
|
|
|
|
def __init__(self, interpreter):
|
|
|
|
super().__init__(interpreter)
|
|
|
|
|
|
|
|
@stringArgs
|
|
|
|
@permittedKwargs({'configure_options', 'cross_configure_options', 'verbose', 'env'})
|
|
|
|
def add_project(self, state, args, kwargs):
|
|
|
|
if len(args) != 1:
|
|
|
|
raise InterpreterException('add_project takes exactly one positional argument')
|
|
|
|
configure_command = args[0]
|
|
|
|
configure_options = kwargs.get('configure_options', [])
|
|
|
|
cross_configure_options = kwargs.get('cross_configure_options', ['--host={host}'])
|
|
|
|
verbose = kwargs.get('verbose', False)
|
|
|
|
env = self.interpreter.unpack_env_kwarg(kwargs)
|
|
|
|
project = ExternalProject(self.interpreter,
|
|
|
|
state.subdir,
|
|
|
|
state.project_version,
|
|
|
|
state.subproject,
|
|
|
|
state.environment,
|
|
|
|
state.build_machine,
|
|
|
|
state.host_machine,
|
|
|
|
configure_command,
|
|
|
|
configure_options,
|
|
|
|
cross_configure_options,
|
|
|
|
env, verbose)
|
|
|
|
return ModuleReturnValue(project, project.targets)
|
|
|
|
|
|
|
|
|
|
|
|
def initialize(*args, **kwargs):
|
|
|
|
return ExternalProjectModule(*args, **kwargs)
|