# 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)