mirror of
https://github.com/mesonbuild/meson.git
synced 2026-11-06 05:16:42 +00:00
4328 lines
213 KiB
Python
4328 lines
213 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright 2012-2021 The Meson development team
|
|
# Copyright © 2023-2025 Intel Corporation
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import io, sys, traceback
|
|
import dataclasses
|
|
import functools
|
|
|
|
from .. import mparser
|
|
from .. import environment
|
|
from .. import coredata
|
|
from .. import dependencies
|
|
from .. import mlog
|
|
from .. import options
|
|
from .. import build
|
|
from .. import compilers
|
|
from .. import envconfig
|
|
from ..wrap import wrap, WrapMode
|
|
from .. import mesonlib
|
|
from ..mesonlib import (EnvironmentVariables, ExecutableSerialisation, MesonBugException, MesonException, HoldableObject,
|
|
FileMode, InstallScriptFailure, MachineChoice, PerMachine, PerMachineDefaultable, is_parent_path,
|
|
listify, has_path_sep, path_has_root, path_is_in_root)
|
|
from ..options import OptionKey
|
|
from ..programs import ExternalProgram, NonExistingExternalProgram, Program
|
|
from ..dependencies import Dependency
|
|
from ..depfile import DepFile
|
|
from ..interpreterbase import ContainerTypeInfo, InterpreterBase, KwargInfo, TypedArgs, PosArgInfo, OptArgInfo, VarArgInfo
|
|
from ..interpreterbase import noArgsFlattening, unholder_return
|
|
from .decorators import apply_machine_map
|
|
from ..interpreterbase import InterpreterException, InvalidArguments, InvalidCode, SubdirDoneRequest
|
|
from ..interpreterbase import Disabler, disablerIfNotFound
|
|
from ..interpreterbase import FeatureFixed, FeatureNew, FeatureDeprecated, FeatureBroken
|
|
from ..interpreterbase import ObjectHolder, ContextManagerObject, DefaultObject
|
|
from ..interpreterbase import stringifyUserArguments, Feature, FeatureValue
|
|
from ..modules import ExtensionModule, ModuleObject, MutableModuleObject, NewExtensionModule, NotFoundExtensionModule, __path__ as modules_path
|
|
from ..optinterpreter import OPTNAME_REGEX
|
|
|
|
from . import interpreterobjects as OBJ
|
|
from . import compiler as compilerOBJ
|
|
from .mesonmain import MesonMain
|
|
from .dependencyfallbacks import DependencyFallbacksHolder
|
|
from .interpreterobjects import (
|
|
SubprojectHolder,
|
|
Test,
|
|
RunProcess,
|
|
extract_required_kwarg,
|
|
extract_search_dirs,
|
|
NullSubprojectInterpreter,
|
|
)
|
|
from .type_checking import (
|
|
BUILD_TARGET_KWS,
|
|
COMMAND_KW,
|
|
CT_BUILD_ALWAYS,
|
|
CT_BUILD_ALWAYS_STALE,
|
|
CT_BUILD_BY_DEFAULT,
|
|
CT_INPUT_KW,
|
|
CT_INSTALL_DIR_KW,
|
|
EXCLUSIVE_EXECUTABLE_KWS,
|
|
EXECUTABLE_KWS,
|
|
JAR_KWS,
|
|
LIBRARY_KWS,
|
|
MULTI_OUTPUT_KW,
|
|
OUTPUT_KW,
|
|
DEFAULT_OPTIONS,
|
|
DEPENDENCIES_KW,
|
|
DEPENDENCY_KWS,
|
|
DEPENDS_KW,
|
|
DEPEND_FILES_KW,
|
|
DEPFILE_KW,
|
|
DISABLER_KW,
|
|
D_MODULE_VERSIONS_KW,
|
|
ENV_KW,
|
|
ENV_METHOD_KW,
|
|
ENV_SEPARATOR_KW,
|
|
EMBED_DIRECTORIES,
|
|
INCLUDE_DIRECTORIES,
|
|
INSTALL_KW,
|
|
INSTALL_DIR_KW,
|
|
INSTALL_MODE_KW,
|
|
INSTALL_FOLLOW_SYMLINKS,
|
|
LINK_ARGS_KW,
|
|
LINK_WITH_KW,
|
|
LINK_WHOLE_KW,
|
|
CT_INSTALL_TAG_KW,
|
|
INSTALL_TAG_KW,
|
|
LANGUAGE_KW,
|
|
NATIVE_KW,
|
|
PRESERVE_PATH_KW,
|
|
REQUIRED_KW,
|
|
SHARED_LIB_KWS,
|
|
SHARED_MOD_KWS,
|
|
DEPENDENCY_SOURCES_KW,
|
|
STATIC_LIB_KWS,
|
|
VARIABLES_KW,
|
|
TEST_KWS,
|
|
BOOL_PARG,
|
|
INT_OARG,
|
|
INT_PARG,
|
|
OBJ_OARG,
|
|
OBJ_PARG,
|
|
OBJ_VARG,
|
|
SRC_VARG,
|
|
STR_FILE_VARG,
|
|
STR_OARG,
|
|
STR_PARG,
|
|
STR_VARG,
|
|
STR_VARG_1,
|
|
TGT_PARG,
|
|
NoneType,
|
|
in_set_validator,
|
|
env_convertor,
|
|
env_validator,
|
|
)
|
|
from . import primitives as P_OBJ
|
|
|
|
from pathlib import Path
|
|
from enum import Enum
|
|
import os
|
|
import shutil
|
|
import uuid
|
|
import re
|
|
import stat
|
|
import collections
|
|
import typing as T
|
|
import textwrap
|
|
import importlib
|
|
import itertools
|
|
|
|
if T.TYPE_CHECKING:
|
|
from typing_extensions import Literal
|
|
import types
|
|
|
|
from .. import cargo
|
|
from . import kwargs as kwtypes
|
|
from ..backend.backends import Backend
|
|
from ..compilers.compilers import CompilerDict, Language
|
|
from ..interpreterbase import FeatureCheckBase
|
|
from ..interpreterbase.baseobjects import InterpreterObject, TYPE_var, TYPE_kwargs
|
|
from ..options import OptionDict
|
|
from ..mesonlib import InstallScript, SubProject
|
|
from ..cmdline import SharedCMDOptions
|
|
from .type_checking import SourcesVarargsType, FullEnvInitValueType
|
|
|
|
# Input source types passed to Targets
|
|
SourceInputs = T.Union[str, build.TargetSources, build.BuildTarget,
|
|
build.BothLibraries, build.ExtractedObjects]
|
|
# Input source types passed to the build.Target classes
|
|
SourceOutputs = T.Union[build.TargetSources, build.BuildTarget,
|
|
build.ExtractedObjects, build.StructuredSources]
|
|
# Sources for custom targets, which can also include ExternalProgram
|
|
CustomTargetSources = T.Union[build.TargetSources, build.BuildTarget,
|
|
build.ExtractedObjects, Program]
|
|
|
|
BuildTargetSource = T.Union[str, build.TargetSources, build.StructuredSources]
|
|
|
|
ProgramVersionFunc = T.Callable[[Program], str]
|
|
|
|
TestClass = T.TypeVar('TestClass', bound=Test)
|
|
|
|
_TEST_PARG = PosArgInfo((build.Executable, build.Jar, Program, mesonlib.File, build.CustomTarget, build.CustomTargetIndex))
|
|
_RUN_CMD_TYPES = (build.Executable, Program, compilers.Compiler, mesonlib.File, str)
|
|
|
|
def _project_version_validator(value: T.Union[T.List, str, mesonlib.File, None]) -> T.Optional[str]:
|
|
if isinstance(value, list):
|
|
if len(value) != 1:
|
|
return 'when passed as array must have a length of 1'
|
|
elif not isinstance(value[0], mesonlib.File):
|
|
return 'when passed as array must contain a File'
|
|
return None
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class SandboxViolationError(InterpreterException):
|
|
|
|
"""Exception raised when trying to use files outside of this project
|
|
|
|
:param inputtype: The kind of thing, usually "directory" or "file"
|
|
:param name: The name of the thing being used
|
|
:param subproject: Is this an attempt to use a file from inside a subproject?
|
|
"""
|
|
|
|
inputtype: str
|
|
name: str
|
|
subproject: bool = dataclasses.field(default=True, kw_only=True)
|
|
|
|
def __str__(self) -> str:
|
|
if self.subproject:
|
|
msg = 'a nested subproject'
|
|
else:
|
|
msg = 'outside current (sub)project'
|
|
return f'Sandbox violation: Tried to grab {self.inputtype} {self.name} from {msg}.'
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class BuiltFileByNameError(InterpreterException):
|
|
|
|
name: str
|
|
|
|
def __str__(self) -> str:
|
|
return (f'{self.name!r} is a generated file, and should be passed by reference instead of string name. '
|
|
'This will become a hard error in Meson 2.0')
|
|
|
|
|
|
class InterpreterRuleRelaxation(Enum):
|
|
''' Defines specific relaxations of the Meson rules.
|
|
|
|
This is intended to be used for automatically converted
|
|
projects (CMake subprojects, build system mixing) that
|
|
generate a Meson AST via introspection, etc.
|
|
'''
|
|
|
|
ALLOW_BUILD_DIR_FILE_REFERENCES = 1
|
|
CARGO_SUBDIR = 2
|
|
ALLOW_SANDBOX_VIOLATION = 4
|
|
|
|
implicit_check_false_warning = """You should add the boolean check kwarg to the run_command call.
|
|
It currently defaults to false,
|
|
but it will default to true in meson 2.0.
|
|
See also: https://github.com/mesonbuild/meson/issues/9300"""
|
|
class Interpreter(InterpreterBase, HoldableObject):
|
|
|
|
backend: mesonlib.late_property[Backend] = mesonlib.late_property()
|
|
|
|
def __init__(
|
|
self,
|
|
_build: build.Build,
|
|
backend: T.Optional[Backend] = None,
|
|
subproject: SubProject = mesonlib.ROOT_SUBPROJECT,
|
|
subdir: str = '',
|
|
subproject_dir: str = 'subprojects',
|
|
invoker_method_default_options: T.Optional[OptionDict] = None,
|
|
ast: T.Optional[mparser.CodeBlockNode] = None,
|
|
relaxations: T.Optional[T.Dict[InterpreterRuleRelaxation, T.Optional[FeatureCheckBase]]] = None,
|
|
user_defined_options: T.Optional[SharedCMDOptions] = None,
|
|
cargo: T.Optional[cargo.Interpreter] = None,
|
|
) -> None:
|
|
super().__init__(_build.environment.get_source_dir(), subdir, subproject, subproject_dir, _build.environment)
|
|
self.active_projectname = ''
|
|
self.build = _build
|
|
self.n_targets = 0
|
|
if backend is not None:
|
|
self.backend = backend
|
|
self.cargo = cargo
|
|
self.modules: T.Dict[str, NewExtensionModule] = {}
|
|
self.relaxations = relaxations or {}
|
|
if ast is None:
|
|
self.load_root_meson_file()
|
|
else:
|
|
self.ast = ast
|
|
self.sanity_check_ast()
|
|
self.builtin.update({'meson': MesonMain(self.build, self)})
|
|
self.validated_cache: T.Set[str] = set()
|
|
self.project_args_frozen = False
|
|
self.global_args_frozen = False # implies self.project_args_frozen
|
|
self.subprojects: PerMachine[T.Dict[str, SubprojectHolder]] = PerMachineDefaultable.default(
|
|
self.environment.is_cross_build(), {}, {})
|
|
self.subproject_stack: T.List[T.Tuple[str, MachineChoice]] = []
|
|
self.configure_file_outputs: T.Dict[str, int] = {}
|
|
# Passed from the outside, only used in subprojects.
|
|
if invoker_method_default_options:
|
|
assert isinstance(invoker_method_default_options, dict)
|
|
self.invoker_method_default_options = invoker_method_default_options
|
|
else:
|
|
self.invoker_method_default_options = {}
|
|
self.project_default_options: OptionDict = {}
|
|
self.build_func_dict()
|
|
self.build_holder_map()
|
|
self.user_defined_options = user_defined_options
|
|
# Languages added in the current subproject
|
|
self.compilers: PerMachine[CompilerDict] = PerMachine({}, {})
|
|
self.parse_project()
|
|
self._redetect_machines()
|
|
|
|
def __getnewargs_ex__(self) -> T.Tuple[T.Tuple[object], T.Dict[str, object]]:
|
|
raise MesonBugException('This class is unpicklable')
|
|
|
|
@contextlib.contextmanager
|
|
def relaxing(self, relaxation: InterpreterRuleRelaxation, feature: FeatureCheckBase) -> T.Iterator[None]:
|
|
'''Temporarily add a rule relaxation to the interpreter, with a
|
|
warning if it is encountered.'''
|
|
if relaxation in self.relaxations:
|
|
yield
|
|
else:
|
|
try:
|
|
self.relaxations[relaxation] = feature
|
|
yield
|
|
finally:
|
|
del self.relaxations[relaxation]
|
|
|
|
def relaxed(self, relaxation: InterpreterRuleRelaxation) -> bool:
|
|
'''Check if a relaxation is in place, and if so whether it should warn
|
|
as a broken or deprecated feature.'''
|
|
if relaxation not in self.relaxations:
|
|
return False
|
|
feature = self.relaxations[relaxation]
|
|
if feature is not None:
|
|
feature.use(self.subproject, location=self.current_node)
|
|
return True
|
|
|
|
def load_root_cargo_lock_file(self) -> None:
|
|
cargo_lock = os.path.join(self.source_root, self.subdir, 'Cargo.lock')
|
|
if not os.path.isfile(cargo_lock):
|
|
return
|
|
from .. import cargo
|
|
try:
|
|
self.cargo = cargo.Interpreter(self.environment, self.subdir, self.subproject_dir)
|
|
except cargo.TomlImplementationMissing:
|
|
# Error delayed to actual usage of a Cargo subproject. The warning
|
|
# has already been printed by Resolver.load_wraps().
|
|
pass
|
|
|
|
def _redetect_machines(self) -> None:
|
|
# Re-initialize machine descriptions. We can do a better job now because we
|
|
# have the compilers needed to gain more knowledge, so wipe out old
|
|
# inference and start over.
|
|
self.build.environment.update_build_machine()
|
|
|
|
self.builtin['build_machine'] = \
|
|
OBJ.MachineHolder(self.build.environment.machines[self.build.machine_map.build], self)
|
|
self.builtin['host_machine'] = \
|
|
OBJ.MachineHolder(self.build.environment.machines[self.build.machine_map.host], self)
|
|
self.builtin['target_machine'] = \
|
|
OBJ.MachineHolder(self.build.environment.machines[self.build.machine_map.target], self)
|
|
|
|
def is_internal_machine(self, for_machine: MachineChoice) -> bool:
|
|
# Return whether applying the caller's effect to the build and
|
|
# host machine would duplicate the effect
|
|
return self.build.machine_map[for_machine] is MachineChoice.BUILD
|
|
|
|
def apply_machine_map_to_kwargs(self, kwargs: kwtypes.BaseBuildTarget | kwtypes.MachineMapArgs) -> None:
|
|
orig_for_machine: MachineChoice | None = kwargs.get('native', None)
|
|
if orig_for_machine is None:
|
|
return
|
|
|
|
for_machine = self.build.machine_map[orig_for_machine]
|
|
if for_machine is orig_for_machine:
|
|
return
|
|
|
|
if 'install' in kwargs and for_machine is MachineChoice.BUILD:
|
|
kwargs['install'] = False
|
|
kwargs['native'] = for_machine
|
|
|
|
def build_func_dict(self) -> None:
|
|
self.funcs.update({'add_global_arguments': self.func_add_global_arguments,
|
|
'add_global_link_arguments': self.func_add_global_link_arguments,
|
|
'add_languages': self.func_add_languages,
|
|
'add_project_arguments': self.func_add_project_arguments,
|
|
'add_project_dependencies': self.func_add_project_dependencies,
|
|
'add_project_link_arguments': self.func_add_project_link_arguments,
|
|
'add_test_setup': self.func_add_test_setup,
|
|
'alias_target': self.func_alias_target,
|
|
'assert': self.func_assert,
|
|
'benchmark': self.func_benchmark,
|
|
'both_libraries': self.func_both_lib,
|
|
'build_target': self.func_build_target,
|
|
'configuration_data': self.func_configuration_data,
|
|
'configure_file': self.func_configure_file,
|
|
'custom_target': self.func_custom_target,
|
|
'debug': self.func_debug,
|
|
'declare_dependency': self.func_declare_dependency,
|
|
'dependency': self.func_dependency,
|
|
'disabler': self.func_disabler,
|
|
'default': self.func_default,
|
|
'environment': self.func_environment,
|
|
'error': self.func_error,
|
|
'executable': self.func_executable,
|
|
'files': self.func_files,
|
|
'find_program': self.func_find_program,
|
|
'generator': self.func_generator,
|
|
'get_option': self.func_get_option,
|
|
'get_variable': self.func_get_variable,
|
|
'import': self.func_import,
|
|
'include_directories': self.func_include_directories,
|
|
'install_data': self.func_install_data,
|
|
'install_emptydir': self.func_install_emptydir,
|
|
'install_headers': self.func_install_headers,
|
|
'install_man': self.func_install_man,
|
|
'install_subdir': self.func_install_subdir,
|
|
'install_symlink': self.func_install_symlink,
|
|
'is_disabler': self.func_is_disabler,
|
|
'is_variable': self.func_is_variable,
|
|
'jar': self.func_jar,
|
|
'join_paths': self.func_join_paths,
|
|
'library': self.func_library,
|
|
'message': self.func_message,
|
|
'option': self.func_option,
|
|
'project': self.func_project,
|
|
'range': self.func_range,
|
|
'run_command': self.func_run_command,
|
|
'run_target': self.func_run_target,
|
|
'set_variable': self.func_set_variable,
|
|
'structured_sources': self.func_structured_sources,
|
|
'subdir': self.func_subdir,
|
|
'shared_library': self.func_shared_lib,
|
|
'shared_module': self.func_shared_module,
|
|
'static_library': self.func_static_lib,
|
|
'subdir_done': self.func_subdir_done,
|
|
'subproject': self.func_subproject,
|
|
'summary': self.func_summary,
|
|
'test': self.func_test,
|
|
'unset_variable': self.func_unset_variable,
|
|
'vcs_tag': self.func_vcs_tag,
|
|
'warning': self.func_warning,
|
|
})
|
|
if 'MESON_UNIT_TEST' in os.environ:
|
|
self.funcs.update({'exception': self.func_exception})
|
|
if 'MESON_RUNNING_IN_PROJECT_TESTS' in os.environ:
|
|
self.funcs.update({'expect_error': self.func_expect_error})
|
|
|
|
def build_holder_map(self) -> None:
|
|
'''
|
|
Build a mapping of `HoldableObject` types to their corresponding
|
|
`ObjectHolder`s. This mapping is used in `InterpreterBase` to automatically
|
|
holderify all returned values from methods and functions.
|
|
'''
|
|
self.holder_map.update({
|
|
# Primitives
|
|
list: P_OBJ.ArrayHolder,
|
|
dict: P_OBJ.DictHolder,
|
|
int: P_OBJ.IntegerHolder,
|
|
bool: P_OBJ.BooleanHolder,
|
|
str: P_OBJ.StringHolder,
|
|
P_OBJ.MesonVersionString: P_OBJ.MesonVersionStringHolder,
|
|
P_OBJ.DependencyVariableString: P_OBJ.DependencyVariableStringHolder,
|
|
P_OBJ.OptionString: P_OBJ.OptionStringHolder,
|
|
|
|
# Meson types
|
|
mesonlib.File: OBJ.FileHolder,
|
|
build.SharedLibrary: OBJ.SharedLibraryHolder,
|
|
build.StaticLibrary: OBJ.StaticLibraryHolder,
|
|
build.BothLibraries: OBJ.BothLibrariesHolder,
|
|
build.SharedModule: OBJ.SharedModuleHolder,
|
|
build.Executable: OBJ.ExecutableHolder,
|
|
build.Jar: OBJ.JarHolder,
|
|
build.CustomTarget: OBJ.CustomTargetHolder,
|
|
build.CustomTargetIndex: OBJ.CustomTargetIndexHolder,
|
|
build.Generator: OBJ.GeneratorHolder,
|
|
build.GeneratedList: OBJ.GeneratedListHolder,
|
|
build.ExtractedObjects: OBJ.GeneratedObjectsHolder,
|
|
build.RunTarget: OBJ.RunTargetHolder,
|
|
build.AliasTarget: OBJ.AliasTargetHolder,
|
|
build.Headers: OBJ.HeadersHolder,
|
|
build.Man: OBJ.ManHolder,
|
|
build.EmptyDir: OBJ.EmptyDirHolder,
|
|
build.Data: OBJ.DataHolder,
|
|
build.SymlinkData: OBJ.SymlinkDataHolder,
|
|
build.InstallDir: OBJ.InstallDirHolder,
|
|
build.IncludeDirs: OBJ.IncludeDirsHolder,
|
|
mesonlib.EnvironmentVariables: OBJ.EnvironmentVariablesHolder,
|
|
build.StructuredSources: OBJ.StructuredSourcesHolder,
|
|
compilers.RunResult: compilerOBJ.TryRunResultHolder,
|
|
dependencies.ExternalLibrary: OBJ.ExternalLibraryHolder,
|
|
Feature: OBJ.FeatureOptionHolder,
|
|
envconfig.MachineInfo: OBJ.MachineHolder,
|
|
build.ConfigurationData: OBJ.ConfigurationDataHolder,
|
|
})
|
|
|
|
'''
|
|
Build a mapping of `HoldableObject` base classes to their
|
|
corresponding `ObjectHolder`s. The difference to `self.holder_map`
|
|
is that the keys here define an upper bound instead of requiring an
|
|
exact match.
|
|
|
|
The mappings defined here are only used when there was no direct hit
|
|
found in `self.holder_map`.
|
|
'''
|
|
self.bound_holder_map.update({
|
|
dependencies.Dependency: OBJ.DependencyHolder,
|
|
Program: OBJ.ProgramHolder,
|
|
compilers.Compiler: compilerOBJ.CompilerHolder,
|
|
ModuleObject: OBJ.ModuleObjectHolder,
|
|
MutableModuleObject: OBJ.MutableModuleObjectHolder,
|
|
})
|
|
|
|
def append_holder_map(self, held_type: T.Type[mesonlib.HoldableObject], holder_type: T.Type[ObjectHolder]) -> None:
|
|
'''
|
|
Adds one additional mapping to the `holder_map`.
|
|
|
|
The intended use for this function is in the `initialize` method of
|
|
modules to register custom object holders.
|
|
'''
|
|
self.holder_map.update({
|
|
held_type: holder_type
|
|
})
|
|
|
|
def process_new_values(self, invalues: list[TYPE_var | InstallScript] | list[build.GeneratedTypes | mesonlib.File]) -> None:
|
|
for v in invalues:
|
|
if isinstance(v, ObjectHolder):
|
|
raise InterpreterException('Modules must not return ObjectHolders')
|
|
if isinstance(v, build.Target):
|
|
self.add_target(v.name, v)
|
|
elif isinstance(v, list):
|
|
self.process_new_values(v)
|
|
elif isinstance(v, (ExecutableSerialisation, InstallScriptFailure)):
|
|
v.subproject = self.subproject
|
|
self.build.install_scripts.append(v)
|
|
elif isinstance(v, build.Data):
|
|
self.build.data.append(v)
|
|
elif isinstance(v, build.SymlinkData):
|
|
self.build.symlinks.append(v)
|
|
elif isinstance(v, dependencies.InternalDependency):
|
|
# FIXME: This is special cased and not ideal:
|
|
# The first source is our new VapiTarget, the rest are deps
|
|
self.process_new_values([v.sources[0]])
|
|
elif isinstance(v, build.InstallDir):
|
|
self.build.install_dirs.append(v)
|
|
elif isinstance(v, Test):
|
|
self.build.tests.append(v)
|
|
elif isinstance(v, (int, str, bool, Disabler, ObjectHolder, build.GeneratedList,
|
|
ExternalProgram, build.ConfigurationData)):
|
|
pass
|
|
else:
|
|
raise InterpreterException(f'Module returned a value of unknown type {v!r}.')
|
|
|
|
def handle_meson_version(self, pv: str, location: mparser.BaseNode) -> None:
|
|
if not mesonlib.version_compare(coredata.stable_version, pv):
|
|
raise InterpreterException.from_node(f'Meson version is {coredata.version} but project requires {pv}', node=location)
|
|
mesonlib.project_meson_versions[self.subproject] = mesonlib.version_check_to_range([pv])
|
|
|
|
def handle_meson_version_from_ast(self) -> None:
|
|
if not self.ast.lines:
|
|
return
|
|
project = self.ast.lines[0]
|
|
# first line is always project()
|
|
if not isinstance(project, mparser.FunctionNode):
|
|
return
|
|
for kw, val in project.args.kwargs.items():
|
|
assert isinstance(kw, mparser.IdNode), 'for mypy'
|
|
if kw.value == 'meson_version':
|
|
# mypy does not understand "and isinstance"
|
|
if isinstance(val, mparser.StringNode):
|
|
self.handle_meson_version(val.value, val)
|
|
|
|
def get_build_def_files(self) -> T.List[str]:
|
|
if self.cargo:
|
|
self.build_def_files.update(self.cargo.get_build_def_files())
|
|
return list(self.build_def_files)
|
|
|
|
def add_build_def_file(self, f: mesonlib.FileOrString) -> None:
|
|
# Use relative path for files within source directory, and absolute path
|
|
# for system files. Skip files within build directory. Also skip not regular
|
|
# files (e.g. /dev/stdout) Normalize the path to avoid duplicates, this
|
|
# is especially important to convert '/' to '\' on Windows.
|
|
if isinstance(f, mesonlib.File):
|
|
if f.is_built:
|
|
return
|
|
f = os.path.normpath(f.relative_name())
|
|
elif os.path.isfile(f) and not f.startswith('/dev/'):
|
|
srcdir = Path(self.environment.get_source_dir())
|
|
builddir = Path(self.environment.get_build_dir())
|
|
try:
|
|
f_ = Path(f).resolve()
|
|
except OSError:
|
|
f_ = Path(f)
|
|
s = f_.stat()
|
|
# Mypy needs the `sys.platform` check to understand that this is
|
|
# a Windows only path
|
|
if (sys.platform == 'win32' and
|
|
s.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT != 0 and
|
|
s.st_reparse_tag == stat.IO_REPARSE_TAG_APPEXECLINK):
|
|
# This is a Windows Store link which we can't
|
|
# resolve, so just do our best otherwise.
|
|
f_ = f_.parent.resolve() / f_.name
|
|
else:
|
|
raise
|
|
if builddir in f_.parents:
|
|
return
|
|
if srcdir in f_.parents:
|
|
f_ = f_.relative_to(srcdir)
|
|
f = str(f_)
|
|
else:
|
|
return
|
|
if f not in self.build_def_files:
|
|
self.build_def_files.add(f)
|
|
|
|
def get_variables(self) -> T.Dict[str, InterpreterObject]:
|
|
return self.variables
|
|
|
|
def check_stdlibs(self) -> None:
|
|
machine_choices = [MachineChoice.HOST]
|
|
if self.coredata.is_cross_build():
|
|
machine_choices.append(MachineChoice.BUILD)
|
|
for for_machine in machine_choices:
|
|
props = self.build.environment.properties[for_machine]
|
|
for l in self.coredata.compilers[for_machine].keys():
|
|
try:
|
|
di = mesonlib.stringlistify(props.get_stdlib(l))
|
|
except KeyError:
|
|
continue
|
|
if len(di) == 1:
|
|
FeatureNew.single_use('stdlib without variable name', '0.56.0', self.subproject, location=self.current_node)
|
|
kwargs: dependencies.base.DependencyObjectKWs = {'native': for_machine}
|
|
name = l + '_stdlib'
|
|
df = DependencyFallbacksHolder(self, [name], for_machine)
|
|
df.set_fallback(di)
|
|
dep = df.lookup(kwargs, force_fallback=True)
|
|
self.build.stdlibs[for_machine][l] = dep
|
|
|
|
@TypedArgs(
|
|
'import',
|
|
pos_types=[STR_PARG],
|
|
kw_types=[
|
|
REQUIRED_KW.evolve(since='0.59.0'),
|
|
DISABLER_KW.evolve(since='0.59.0'),
|
|
],
|
|
)
|
|
@disablerIfNotFound
|
|
def func_import(self, node: mparser.BaseNode, args: T.Tuple[str],
|
|
kwargs: 'kwtypes.FuncImportModule') -> T.Union[ExtensionModule, NewExtensionModule, NotFoundExtensionModule]:
|
|
modname = args[0]
|
|
disabled, required, _ = extract_required_kwarg(kwargs, self.subproject)
|
|
if disabled:
|
|
return NotFoundExtensionModule(modname)
|
|
|
|
# Always report implementation detail modules don't exist
|
|
if modname.startswith('_'):
|
|
raise InvalidArguments(f'Module "{modname}" does not exist')
|
|
|
|
expect_unstable = False
|
|
# Some tests use "unstable_" instead of "unstable-", and that happens to work because
|
|
# of implementation details
|
|
if modname.startswith(('unstable-', 'unstable_')):
|
|
if modname.startswith('unstable_'):
|
|
mlog.deprecation(f'Importing unstable modules as "{modname}" instead of "{modname.replace("_", "-", 1)}"',
|
|
location=node)
|
|
real_modname = modname[len('unstable') + 1:] # + 1 to handle the - or _
|
|
expect_unstable = True
|
|
else:
|
|
real_modname = modname
|
|
|
|
if real_modname in self.modules:
|
|
return self.modules[real_modname]
|
|
try:
|
|
full_module_path = f'mesonbuild.modules.{real_modname}'
|
|
module = importlib.import_module(full_module_path)
|
|
except ImportError as e:
|
|
if e.name != full_module_path:
|
|
if required:
|
|
raise e
|
|
|
|
mlog.warning(f'Module "{modname}" exists but failed to import.')
|
|
|
|
for line in traceback.format_exception(e):
|
|
mlog.debug(line)
|
|
|
|
if required:
|
|
ustr = f'Module "{modname}" does not exist.'
|
|
from difflib import get_close_matches
|
|
from pkgutil import iter_modules
|
|
modnames = [mod.name for mod in iter_modules(modules_path) if not mod.name.startswith('_')]
|
|
close_matches = get_close_matches(modname, modnames)
|
|
if close_matches:
|
|
ustr += f' Did you mean "{close_matches[0]}"?'
|
|
raise InvalidArguments(ustr)
|
|
ext_module = NotFoundExtensionModule(real_modname)
|
|
else:
|
|
ext_module = module.initialize(self)
|
|
assert isinstance(ext_module, (ExtensionModule, NewExtensionModule)), 'for mypy'
|
|
self.build.modules.add(real_modname)
|
|
if ext_module.INFO.added:
|
|
FeatureNew.single_use(f'module {ext_module.INFO.name}', ext_module.INFO.added, self.subproject, location=node)
|
|
if ext_module.INFO.deprecated:
|
|
FeatureDeprecated.single_use(f'module {ext_module.INFO.name}', ext_module.INFO.deprecated, self.subproject, location=node)
|
|
if expect_unstable and not ext_module.INFO.unstable and ext_module.INFO.stabilized is None:
|
|
raise InvalidArguments(f'Module {ext_module.INFO.name} has never been unstable, remove "unstable-" prefix.')
|
|
if ext_module.INFO.stabilized is not None:
|
|
if expect_unstable:
|
|
FeatureDeprecated.single_use(
|
|
f'module {ext_module.INFO.name} has been stabilized',
|
|
ext_module.INFO.stabilized, self.subproject,
|
|
'drop "unstable-" prefix from the module name',
|
|
location=node)
|
|
else:
|
|
FeatureNew.single_use(
|
|
f'module {ext_module.INFO.name} as stable module',
|
|
ext_module.INFO.stabilized, self.subproject,
|
|
f'Consider either adding "unstable-" to the module name, or updating the meson required version to ">= {ext_module.INFO.stabilized}"',
|
|
location=node)
|
|
elif ext_module.INFO.unstable:
|
|
if not expect_unstable:
|
|
if required:
|
|
raise InvalidArguments(f'Module "{ext_module.INFO.name}" has not been stabilized, and must be imported as unstable-{ext_module.INFO.name}')
|
|
ext_module = NotFoundExtensionModule(real_modname)
|
|
else:
|
|
mlog.warning(f'Module {ext_module.INFO.name} has no backwards or forwards compatibility and might not exist in future releases.', location=node, fatal=False)
|
|
|
|
self.modules[real_modname] = ext_module
|
|
return ext_module
|
|
|
|
@TypedArgs('files', var_types=STR_VARG)
|
|
def func_files(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> T.List[mesonlib.File]:
|
|
return self.source_strings_to_files(args[0])
|
|
|
|
@TypedArgs(
|
|
'declare_dependency',
|
|
kw_types=[
|
|
KwargInfo('compile_args', ContainerTypeInfo(list, str), listify=True, default=[]),
|
|
INCLUDE_DIRECTORIES.evolve(name='d_import_dirs', since='0.62.0'),
|
|
D_MODULE_VERSIONS_KW.evolve(since='0.62.0'),
|
|
EMBED_DIRECTORIES,
|
|
LINK_ARGS_KW,
|
|
DEPENDENCIES_KW,
|
|
INCLUDE_DIRECTORIES.evolve(since_values={ContainerTypeInfo(list, str): '0.50.0'}),
|
|
LINK_WITH_KW,
|
|
LINK_WHOLE_KW.evolve(since='0.46.0'),
|
|
DEPENDENCY_SOURCES_KW,
|
|
KwargInfo('extra_files', ContainerTypeInfo(list, (mesonlib.File, str)), listify=True, default=[], since='1.2.0'),
|
|
VARIABLES_KW.evolve(since='0.54.0', since_values={list: '0.56.0'}),
|
|
KwargInfo('version', (str, NoneType)),
|
|
KwargInfo('objects', ContainerTypeInfo(list, build.ExtractedObjects), listify=True, default=[], since='1.1.0'),
|
|
],
|
|
)
|
|
def func_declare_dependency(self, node: mparser.BaseNode, args: T.List[TYPE_var],
|
|
kwargs: kwtypes.FuncDeclareDependency) -> dependencies.Dependency:
|
|
deps = kwargs['dependencies']
|
|
incs = self.extract_incdirs(kwargs['include_directories'])
|
|
embed_directories = self.extract_incdirs(kwargs['embed_directories'])
|
|
libs = kwargs['link_with']
|
|
libs_whole = kwargs['link_whole']
|
|
objects = kwargs['objects']
|
|
sources = self.source_strings_to_files(kwargs['sources'])
|
|
extra_files = self.source_strings_to_files(kwargs['extra_files'])
|
|
compile_args = kwargs['compile_args']
|
|
link_args = kwargs['link_args']
|
|
variables = kwargs['variables']
|
|
version = kwargs['version']
|
|
if version is None:
|
|
version = self.project_version
|
|
d_module_versions = kwargs['d_module_versions']
|
|
d_import_dirs = self.extract_incdirs(kwargs['d_import_dirs'], True)
|
|
srcdir = self.environment.source_dir
|
|
subproject_dir = os.path.abspath(os.path.join(srcdir, self.subproject_dir))
|
|
project_root = os.path.abspath(os.path.join(srcdir, self.root_subdir))
|
|
# convert variables which refer to an -uninstalled.pc style datadir
|
|
for k, v in variables.items():
|
|
if not v:
|
|
FeatureNew.single_use('empty variable value in declare_dependency', '1.4.0', self.subproject, location=node)
|
|
if path_has_root(v) \
|
|
and (self.is_subproject() or not is_parent_path(subproject_dir, v)) \
|
|
and is_parent_path(project_root, v) \
|
|
and os.path.isdir(v):
|
|
variables[k] = P_OBJ.DependencyVariableString(v)
|
|
|
|
dep = dependencies.InternalDependency(version, incs, embed_directories, compile_args,
|
|
link_args, libs, libs_whole, sources, extra_files,
|
|
deps, variables, d_module_versions, d_import_dirs,
|
|
objects)
|
|
return dep
|
|
|
|
@TypedArgs('assert', pos_types=[BOOL_PARG], opt_types=[STR_OARG.evolve(optional_since='0.53.0')])
|
|
def func_assert(self, node: mparser.FunctionNode, args: T.Tuple[bool, T.Optional[str]],
|
|
kwargs: 'TYPE_kwargs') -> None:
|
|
value, message = args
|
|
|
|
if not value:
|
|
if message is None:
|
|
from ..ast import AstPrinter
|
|
printer = AstPrinter()
|
|
node.args.arguments[0].accept(printer)
|
|
message = printer.result
|
|
raise InterpreterException('Assert failed: ' + message)
|
|
|
|
# Executables aren't actually accepted, but we allow them here to allow for
|
|
# better error messages when overridden
|
|
@TypedArgs(
|
|
'run_command',
|
|
pos_types=[PosArgInfo(_RUN_CMD_TYPES)],
|
|
var_types=VarArgInfo(_RUN_CMD_TYPES),
|
|
kw_types=[
|
|
KwargInfo('check', (bool, NoneType), since='0.47.0'),
|
|
KwargInfo('capture', bool, default=True, since='0.47.0'),
|
|
KwargInfo('console', bool, default=False, since='1.11.0'),
|
|
ENV_KW.evolve(since='0.50.0'),
|
|
],
|
|
)
|
|
def func_run_command(self, node: mparser.BaseNode,
|
|
args: T.Tuple[T.Union[build.Executable, Program, compilers.Compiler, mesonlib.File, str],
|
|
T.List[T.Union[build.Executable, Program, compilers.Compiler, mesonlib.File, str]]],
|
|
kwargs: 'kwtypes.RunCommand') -> RunProcess:
|
|
return self.run_command_impl(args, kwargs)
|
|
|
|
def _compiled_exe_error(self, cmd: T.Union[Program, build.Executable]) -> T.NoReturn:
|
|
descr = cmd.name if isinstance(cmd, build.Executable) else cmd.description()
|
|
for for_machine in MachineChoice:
|
|
for name, exe in self.build.find_overrides[for_machine].items():
|
|
if cmd == exe:
|
|
raise InterpreterException(f'Program {name!r} was overridden with the compiled executable {descr!r} and therefore cannot be used during configuration')
|
|
raise InterpreterException(f'Program {descr!r} is a compiled executable and therefore cannot be used during configuration')
|
|
|
|
def run_command_impl(self,
|
|
args: T.Tuple[T.Union[build.Executable, Program, compilers.Compiler, mesonlib.File, str],
|
|
T.List[T.Union[build.Executable, Program, compilers.Compiler, mesonlib.File, str]]],
|
|
kwargs: 'kwtypes.RunCommand',
|
|
in_builddir: bool = False) -> RunProcess:
|
|
cmd, cargs = args
|
|
env = kwargs['env']
|
|
srcdir = self.environment.get_source_dir()
|
|
builddir = self.environment.get_build_dir()
|
|
|
|
check = kwargs['check']
|
|
if check is None:
|
|
mlog.warning(implicit_check_false_warning, once=True)
|
|
check = False
|
|
|
|
expanded_args: T.List[str] = []
|
|
if isinstance(cmd, build.Executable):
|
|
self._compiled_exe_error(cmd)
|
|
elif isinstance(cmd, Program):
|
|
if not cmd.found():
|
|
raise InterpreterException(f'command {cmd.get_name()!r} not found or not executable')
|
|
if not cmd.runnable():
|
|
self._compiled_exe_error(cmd)
|
|
elif isinstance(cmd, compilers.Compiler):
|
|
expanded_args = cmd.get_exe_args()
|
|
cmd = cmd.get_exe()
|
|
prog = ExternalProgram(cmd, silent=True)
|
|
if not prog.found():
|
|
raise InterpreterException(f'Program {cmd!r} not found or not executable')
|
|
cmd = prog
|
|
else:
|
|
if isinstance(cmd, mesonlib.File):
|
|
cmd = cmd.absolute_path(srcdir, builddir)
|
|
# Prefer scripts in the current source directory
|
|
search_dir = os.path.join(srcdir, self.subdir)
|
|
prog = ExternalProgram(cmd, silent=True, search_dirs=[search_dir])
|
|
if not prog.found():
|
|
raise InterpreterException(f'Program or command {cmd!r} not found or not executable')
|
|
cmd = prog
|
|
for a in cargs:
|
|
if isinstance(a, str):
|
|
expanded_args.append(a)
|
|
elif isinstance(a, mesonlib.File):
|
|
expanded_args.append(a.absolute_path(srcdir, builddir))
|
|
elif isinstance(a, Program):
|
|
if not a.found():
|
|
raise InterpreterException(f'command {cmd.get_name()!r} not found or not executable')
|
|
if not a.runnable():
|
|
self._compiled_exe_error(a)
|
|
expanded_args.append(a.get_path())
|
|
elif isinstance(a, compilers.Compiler):
|
|
FeatureNew.single_use('Compiler object as a variadic argument to `run_command`', '0.61.0', self.subproject, location=self.current_node)
|
|
prog = ExternalProgram(a.get_exe(), silent=True)
|
|
if not prog.found():
|
|
raise InterpreterException(f'Program {cmd!r} not found or not executable')
|
|
expanded_args.append(prog.get_path())
|
|
else:
|
|
self._compiled_exe_error(a)
|
|
|
|
# If any file that was used as an argument to the command
|
|
# changes, we must re-run the configuration step.
|
|
self.add_build_def_file(cmd.get_path())
|
|
for a in expanded_args:
|
|
a = os.path.join(builddir if in_builddir else srcdir, self.subdir, a)
|
|
self.add_build_def_file(a)
|
|
|
|
return RunProcess(cmd, expanded_args, env, srcdir, builddir, self.subdir,
|
|
self.environment.get_build_command() + ['introspect'],
|
|
in_builddir=in_builddir, check=check, capture=kwargs['capture'],
|
|
console=kwargs['console'])
|
|
|
|
def func_option(self, nodes: mparser.BaseNode, args: list[TYPE_var], kwargs: TYPE_kwargs) -> T.NoReturn:
|
|
raise InterpreterException('Tried to call option() in build description file. All options must be in the option file.')
|
|
|
|
@TypedArgs(
|
|
'subproject',
|
|
pos_types=[STR_PARG],
|
|
kw_types=[
|
|
REQUIRED_KW,
|
|
NATIVE_KW.evolve(since='1.12.0'),
|
|
DEFAULT_OPTIONS.evolve(since='0.38.0'),
|
|
KwargInfo('version', ContainerTypeInfo(list, str), default=[], listify=True),
|
|
],
|
|
)
|
|
def func_subproject(self, nodes: mparser.BaseNode, args: T.Tuple[SubProject], kwargs: kwtypes.Subproject) -> SubprojectHolder:
|
|
# Note that this does not use @apply_machine_map. kw['for_machine']
|
|
# must be the machine as requested so that it takes two *explicit*
|
|
# levels of "native: true" for the target_machine to become the
|
|
# build_machine. See _do_subproject_meson().
|
|
kw: kwtypes.DoSubproject = {
|
|
'required': kwargs['required'],
|
|
'default_options': kwargs['default_options'],
|
|
'version': kwargs['version'],
|
|
'options': None,
|
|
'cmake_options': [],
|
|
'for_machine': kwargs['native'],
|
|
}
|
|
return self.do_subproject(args[0], kw)
|
|
|
|
def disabled_subproject(self, subp_name: SubProject, for_machine: MachineChoice,
|
|
disabled_feature: T.Optional[str] = None,
|
|
exception: T.Optional[Exception] = None) -> SubprojectHolder:
|
|
sub = SubprojectHolder(NullSubprojectInterpreter(), os.path.join(self.subproject_dir, subp_name),
|
|
disabled_feature=disabled_feature, exception=exception)
|
|
self.subprojects[for_machine][subp_name] = sub
|
|
return sub
|
|
|
|
def create_build_subdir(self, subdir: str) -> None:
|
|
os.makedirs(os.path.join(self.build.environment.get_build_dir(), subdir), exist_ok=True)
|
|
|
|
@staticmethod
|
|
def format_subproject_stack(stack: T.List[T.Tuple[str, MachineChoice]], join: str = ' => ') -> str:
|
|
prefix = ''
|
|
result = ''
|
|
for_build = False
|
|
for subp_name, machine in stack:
|
|
result += prefix + subp_name
|
|
if machine is MachineChoice.BUILD and not for_build:
|
|
result += ' (build)'
|
|
for_build = True
|
|
prefix = join
|
|
return result
|
|
|
|
def do_subproject(self, subp_name: SubProject, kwargs: kwtypes.DoSubproject, force_method: T.Optional[wrap.Method] = None,
|
|
forced_options: T.Optional[OptionDict] = None) -> SubprojectHolder:
|
|
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
|
|
# kwargs['for_machine'] is used to choose between Build.copy() and
|
|
# Build.copy_for_build_machine(), while for_machine is used to
|
|
# look up and register the subproject.
|
|
kwargs.setdefault('for_machine', MachineChoice.HOST)
|
|
for_machine = self.build.machine_map[kwargs['for_machine']]
|
|
|
|
if disabled:
|
|
mlog.log('Subproject', mlog.bold(subp_name), ':', 'skipped: feature', mlog.bold(feature), 'disabled')
|
|
return self.disabled_subproject(subp_name, for_machine, disabled_feature=feature)
|
|
|
|
default_options = kwargs['default_options']
|
|
|
|
# This in practice is only used for default_library. forced_options is the
|
|
# only case in which a meson.build file overrides the machine file or the
|
|
# command line.
|
|
if forced_options:
|
|
for k, v in forced_options.items():
|
|
# FIXME: this should have no business poking at augments[],
|
|
# but set_option() does not do what we want
|
|
self.coredata.optstore.augments[k.evolve(subproject=subp_name)] = v
|
|
default_options = {**forced_options, **default_options}
|
|
|
|
if subp_name == '':
|
|
raise InterpreterException('Subproject name must not be empty.')
|
|
if subp_name[0] == '.':
|
|
raise InterpreterException('Subproject name must not start with a period.')
|
|
if '..' in subp_name:
|
|
raise InterpreterException('Subproject name must not contain a ".." path segment.')
|
|
if path_has_root(subp_name):
|
|
raise InterpreterException('Subproject name must not be an absolute path.')
|
|
if has_path_sep(subp_name):
|
|
mlog.warning('Subproject name has a path separator. This may cause unexpected behaviour.',
|
|
location=self.current_node)
|
|
fullstack = self.subproject_stack + [(subp_name, for_machine)]
|
|
if (subp_name, for_machine) in self.subproject_stack:
|
|
incpath = self.format_subproject_stack(fullstack)
|
|
raise InvalidCode(f'Recursive include of subprojects: {incpath}.')
|
|
if subp_name in self.subprojects[for_machine]:
|
|
subproject = self.subprojects[for_machine][subp_name]
|
|
if required and not subproject.found():
|
|
raise InterpreterException(f'Subproject "{subproject.subdir}" required but not found.')
|
|
if kwargs['version']:
|
|
pv = self.build.projects[for_machine][subp_name].version
|
|
wanted = kwargs['version']
|
|
if pv == 'undefined' or not mesonlib.version_compare_many(pv, wanted)[0]:
|
|
raise InterpreterException(f'Subproject {subp_name} version is {pv} but {wanted} required.')
|
|
return subproject
|
|
|
|
r = self.environment.wrap_resolver
|
|
try:
|
|
subdir, method = r.resolve(subp_name, force_method)
|
|
except wrap.WrapException as e:
|
|
if force_method is not None:
|
|
prefix = force_method.title() + ' subproject'
|
|
else:
|
|
prefix = 'Subproject'
|
|
msg = [prefix, mlog.bold(subp_name), 'is buildable:', mlog.red('NO')]
|
|
if not required:
|
|
mlog.log(e)
|
|
mlog.log(*msg, '(disabling)')
|
|
return self.disabled_subproject(subp_name, for_machine, exception=e)
|
|
mlog.error(*msg)
|
|
raise e
|
|
|
|
self.create_build_subdir(subdir)
|
|
self.global_args_frozen = True
|
|
|
|
stack = self.format_subproject_stack(fullstack, ':')
|
|
m = ['\nExecuting subproject', mlog.bold(stack)]
|
|
if method != 'meson':
|
|
m += ['method', mlog.bold(method)]
|
|
m.extend(['for machine:', mlog.bold(for_machine.get_lower_case_name())])
|
|
mlog.log(*m, '\n', nested=False)
|
|
|
|
methods_map: T.Dict[wrap.Method, T.Callable[[SubProject, MachineChoice, str, OptionDict, kwtypes.DoSubproject],
|
|
SubprojectHolder]] = {
|
|
'meson': self._do_subproject_meson,
|
|
'cmake': self._do_subproject_cmake,
|
|
'cargo': self._do_subproject_cargo,
|
|
}
|
|
|
|
# The subprojects dictionary is shared with the subproject's interpreter, so it
|
|
# keeps the nested subprojects even if this one fails. Remember what was
|
|
# there to forget the rest.
|
|
configured = {m: set(self.subprojects[m]) for m in MachineChoice}
|
|
try:
|
|
return methods_map[method](subp_name, for_machine, subdir, default_options, kwargs)
|
|
# Invalid code is always an error
|
|
except InvalidCode:
|
|
raise
|
|
except Exception as e:
|
|
if not required:
|
|
# The Build of the failed subproject is discarded, including the
|
|
# dependency overrides and the projects of the nested subprojects.
|
|
for m in MachineChoice:
|
|
for name in set(self.subprojects[m]) - configured[m]:
|
|
del self.subprojects[m][name]
|
|
with mlog.nested(subp_name):
|
|
# Suppress the 'ERROR:' prefix because this exception is not
|
|
# fatal and VS CI treat any logs with "ERROR:" as fatal.
|
|
mlog.exception(e, prefix=mlog.yellow('Exception:'))
|
|
mlog.log('\nSubproject', mlog.bold(subdir), 'is buildable:', mlog.red('NO'), '(disabling)')
|
|
return self.disabled_subproject(subp_name, for_machine, exception=e)
|
|
raise e
|
|
|
|
def _save_ast(self, subdir: str, ast: mparser.CodeBlockNode) -> None:
|
|
# Debug print the generated meson file
|
|
from ..ast import AstIndentationGenerator, AstPrinter
|
|
printer = AstPrinter(update_ast_line_nos=True)
|
|
ast.accept(AstIndentationGenerator())
|
|
ast.accept(printer)
|
|
printer.post_process()
|
|
meson_filename = os.path.join(self.build.environment.get_build_dir(), subdir, 'meson.build')
|
|
with open(meson_filename, "w", encoding='utf-8') as f:
|
|
f.write(printer.result)
|
|
mlog.log('Generated Meson AST:', meson_filename)
|
|
mlog.cmd_ci_include(meson_filename)
|
|
|
|
def _do_subproject_meson(self, subp_name: SubProject,
|
|
for_machine: MachineChoice, subdir: str,
|
|
default_options: OptionDict,
|
|
kwargs: kwtypes.DoSubproject,
|
|
ast: T.Optional[mparser.CodeBlockNode] = None,
|
|
build_def_files: T.Optional[T.List[str]] = None,
|
|
relaxations: T.Optional[T.Dict[InterpreterRuleRelaxation, T.Optional[FeatureCheckBase]]] = None,
|
|
cargo: T.Optional[cargo.Interpreter] = None) -> SubprojectHolder:
|
|
# The machine in kwargs is the one that the caller requested. Use it to choose
|
|
# the kind of Build, so that "native: false" in a subproject for the build
|
|
# machine keeps the target machine.
|
|
if kwargs['for_machine'] is MachineChoice.BUILD:
|
|
new_build = self.build.copy_for_build_machine()
|
|
else:
|
|
new_build = self.build.copy()
|
|
|
|
with mlog.nested(subp_name):
|
|
if ast:
|
|
self._save_ast(subdir, ast)
|
|
|
|
subi = Interpreter(new_build, self.backend, subp_name, subdir, self.subproject_dir,
|
|
default_options, ast=ast, relaxations=relaxations,
|
|
user_defined_options=self.user_defined_options,
|
|
cargo=cargo)
|
|
# Those lists are shared by all interpreters. That means that
|
|
# even if the subproject fails, any modification that the subproject
|
|
# made to those lists will affect the parent project.
|
|
subi.subprojects = self.subprojects
|
|
subi.modules = self.modules
|
|
subi.holder_map = self.holder_map
|
|
subi.bound_holder_map = self.bound_holder_map
|
|
|
|
subi.subproject_stack = self.subproject_stack + [(subp_name, for_machine)]
|
|
current_active = self.active_projectname
|
|
with mlog.nested_warnings():
|
|
subi.run()
|
|
subi_warnings = mlog.get_warning_count()
|
|
mlog.log('Subproject', mlog.bold(subp_name), 'finished.')
|
|
|
|
mlog.log()
|
|
|
|
if kwargs['version']:
|
|
pv = subi.project_version
|
|
wanted = kwargs['version']
|
|
if pv == 'undefined' or not mesonlib.version_compare_many(pv, wanted)[0]:
|
|
raise InterpreterException(f'Subproject {subp_name} version is {pv} but {wanted} required.')
|
|
self.active_projectname = current_active
|
|
self.subprojects[for_machine].update(subi.subprojects[for_machine])
|
|
self.subprojects[for_machine][subp_name] = SubprojectHolder(
|
|
subi, subdir, warnings=subi_warnings, callstack=self.subproject_stack)
|
|
# Duplicates are possible when subproject uses files from project root
|
|
if build_def_files:
|
|
self.build_def_files.update(build_def_files)
|
|
# We always need the subi.build_def_files, to propagate sub-sub-projects
|
|
self.build_def_files.update(subi.get_build_def_files())
|
|
self.build.merge(subi.build)
|
|
return self.subprojects[for_machine][subp_name]
|
|
|
|
def _do_subproject_cmake(self, subp_name: SubProject,
|
|
for_machine: MachineChoice, subdir: str,
|
|
default_options: OptionDict,
|
|
kwargs: kwtypes.DoSubproject) -> SubprojectHolder:
|
|
from ..cmake import CMakeInterpreter
|
|
with mlog.nested(subp_name):
|
|
from ..modules.cmake import CMakeSubprojectOptions
|
|
kw_opts = kwargs.get('options') or CMakeSubprojectOptions()
|
|
cmake_options = kwargs.get('cmake_options', []) + kw_opts.cmake_options
|
|
cm_int = CMakeInterpreter(Path(subdir), self.build.environment, self.backend, for_machine)
|
|
cm_int.initialise(cmake_options)
|
|
cm_int.analyse()
|
|
|
|
# Generate a meson ast and execute it with the normal do_subproject_meson
|
|
ast = cm_int.pretend_to_be_meson(kw_opts.target_options)
|
|
result = self._do_subproject_meson(
|
|
subp_name, for_machine, subdir,
|
|
default_options, kwargs, ast,
|
|
[str(f) for f in cm_int.bs_files],
|
|
relaxations={
|
|
InterpreterRuleRelaxation.ALLOW_BUILD_DIR_FILE_REFERENCES: None,
|
|
}
|
|
)
|
|
result.cm_interpreter = cm_int
|
|
return result
|
|
|
|
def _do_subproject_cargo(self, subp_name: SubProject,
|
|
for_machine: MachineChoice, subdir: str,
|
|
default_options: OptionDict,
|
|
kwargs: kwtypes.DoSubproject) -> SubprojectHolder:
|
|
from .. import cargo
|
|
FeatureNew.single_use('Cargo subproject', '1.3.0', self.subproject, location=self.current_node)
|
|
mlog.warning('Cargo subproject is an experimental feature and has no backwards compatibility guarantees.',
|
|
once=True, location=self.current_node)
|
|
self.add_languages(['rust'], True, MachineChoice.HOST)
|
|
self.add_languages(['rust'], True, MachineChoice.BUILD)
|
|
with mlog.nested(subp_name):
|
|
try:
|
|
cargo_int = self.cargo or cargo.Interpreter(self.environment, subdir, self.subproject_dir)
|
|
except cargo.TomlImplementationMissing as e:
|
|
raise MesonException(f'Failed to load Cargo.lock: {e!s}')
|
|
|
|
if os.path.exists(os.path.join(self.environment.get_source_dir(), subdir, environment.build_filename)):
|
|
ast = None
|
|
else:
|
|
ast = cargo_int.interpret(subdir)
|
|
|
|
return self._do_subproject_meson(
|
|
subp_name, for_machine, subdir, default_options, kwargs, ast,
|
|
relaxations={InterpreterRuleRelaxation.CARGO_SUBDIR: None} if ast is not None else None,
|
|
cargo=cargo_int)
|
|
|
|
@TypedArgs('get_option', pos_types=[STR_PARG])
|
|
def func_get_option(self, node: mparser.BaseNode, args: T.Tuple[str],
|
|
kwargs: TYPE_kwargs) -> T.Union[options.UserOption, 'TYPE_var']:
|
|
optname = args[0]
|
|
|
|
if ':' in optname:
|
|
raise InterpreterException('Having a colon in option name is forbidden, '
|
|
'projects are not allowed to directly access '
|
|
'options of other subprojects.')
|
|
if OPTNAME_REGEX.search(optname.split('.', maxsplit=1)[-1]) is not None:
|
|
raise InterpreterException(f'Invalid option name {optname!r}')
|
|
|
|
option_object: T.Optional[options.AnyOptionType]
|
|
|
|
try:
|
|
optkey = options.OptionKey.from_string(optname).evolve(subproject=self.subproject)
|
|
option_object, value = self.coredata.optstore.get_option_and_value_for(optkey)
|
|
except KeyError:
|
|
if self.coredata.optstore.is_base_option(optkey):
|
|
# Due to backwards compatibility return the default
|
|
# option for base options instead of erroring out.
|
|
option_object = options.COMPILER_BASE_OPTIONS[optkey.evolve(subproject=None, machine=MachineChoice.HOST)]
|
|
value = option_object.default
|
|
else:
|
|
if self.subproject:
|
|
raise MesonException(f'Option {optname} does not exist for subproject {self.subproject}.')
|
|
raise MesonException(f'Option {optname} does not exist.')
|
|
|
|
if isinstance(option_object, options.UserFeatureOption):
|
|
return Feature(optname, FeatureValue(value))
|
|
elif optname == 'b_sanitize':
|
|
assert isinstance(option_object, options.UserStringArrayOption)
|
|
# To ensure backwards compatibility this always returns a string.
|
|
# We may eventually want to introduce a new "format" kwarg that
|
|
# allows the user to modify this behaviour, but for now this is
|
|
# likely good enough for most usecases.
|
|
if not value:
|
|
return 'none'
|
|
assert isinstance(value, list), 'for mypy'
|
|
return ','.join(sorted(value))
|
|
|
|
if isinstance(value, str):
|
|
return P_OBJ.OptionString(value, f'{{{optname}}}')
|
|
return value
|
|
|
|
@TypedArgs(
|
|
'configuration_data',
|
|
opt_types=[OptArgInfo(ContainerTypeInfo(dict, (str, int, bool)), since='0.49.0')],
|
|
)
|
|
def func_configuration_data(self, node: mparser.BaseNode,
|
|
args: T.Tuple[T.Optional[T.Dict[str, str | int | bool]]],
|
|
kwargs: 'TYPE_kwargs') -> build.ConfigurationData:
|
|
return build.ConfigurationData(args[0])
|
|
|
|
@functools.lru_cache(None)
|
|
def set_backend(self) -> None:
|
|
from ..backend import backends
|
|
|
|
if OptionKey('genvslite') in self.user_defined_options.cmd_line_options:
|
|
# Use of the '--genvslite vsxxxx' option ultimately overrides any '--backend xxx'
|
|
# option the user may specify.
|
|
backend_name = self.coredata.optstore.get_value_for(OptionKey('genvslite'))
|
|
assert isinstance(backend_name, str), 'for mypy'
|
|
self.backend = backends.get_genvslite_backend(backend_name, self.build)
|
|
else:
|
|
backend_name = self.coredata.optstore.get_value_for(OptionKey('backend'))
|
|
assert isinstance(backend_name, str), 'for mypy'
|
|
self.backend = backends.get_backend_from_name(backend_name, self.build)
|
|
|
|
if backend_name != self.backend.name:
|
|
if self.backend.name.startswith('vs'):
|
|
mlog.log('Auto detected Visual Studio backend:', mlog.bold(self.backend.name))
|
|
if not self.environment.first_invocation:
|
|
raise MesonBugException(f'Backend changed from {backend_name} to {self.backend.name}')
|
|
self.coredata.optstore.set_option(OptionKey('backend'), self.backend.name, first_invocation=True)
|
|
|
|
self.environment.init_backend_options(backend_name)
|
|
|
|
def _validate_languages(self, langs: T.List[str], required: bool, node: mparser.BaseNode) -> T.List[Language]:
|
|
valid: T.List[Language] = []
|
|
|
|
for lang in langs:
|
|
lang = lang.lower()
|
|
if lang in compilers.all_languages:
|
|
valid.append(T.cast('Language', lang))
|
|
continue
|
|
|
|
FeatureBroken.single_use(
|
|
f'Adding unknown language {lang}', '1.11.0', self.subproject,
|
|
'This language cannot be found. It could be: a typo, in which case it can be removed, or, '
|
|
'this project might have its Meson version requirements set incorrectly.',
|
|
node)
|
|
if required:
|
|
raise InterpreterException.from_node(
|
|
f'Attempted to add language "{lang}", which is not known to this version of Meson.',
|
|
node=node)
|
|
|
|
return valid
|
|
|
|
@TypedArgs(
|
|
'project',
|
|
pos_types=[STR_PARG],
|
|
var_types=STR_VARG,
|
|
kw_types=[
|
|
DEFAULT_OPTIONS,
|
|
KwargInfo('meson_version', (str, NoneType)),
|
|
KwargInfo(
|
|
'version',
|
|
(str, mesonlib.File, list),
|
|
default='undefined',
|
|
validator=_project_version_validator,
|
|
convertor=lambda x: x[0] if isinstance(x, list) else x,
|
|
),
|
|
KwargInfo('license', (ContainerTypeInfo(list, str), NoneType), default=None, listify=True),
|
|
KwargInfo('license_files', ContainerTypeInfo(list, str), default=[], listify=True, since='1.1.0'),
|
|
KwargInfo('subproject_dir', str, default='subprojects'),
|
|
],
|
|
)
|
|
def func_project(self, node: mparser.FunctionNode, args: T.Tuple[str, T.List[str]], kwargs: 'kwtypes.Project') -> None:
|
|
proj_name, proj_langs_ = args
|
|
if ':' in proj_name:
|
|
raise InvalidArguments(f"Project name {proj_name!r} must not contain ':'")
|
|
|
|
proj_langs = self._validate_languages(proj_langs_, True, node)
|
|
|
|
# This needs to be evaluated as early as possible, as meson uses this
|
|
# for things like deprecation testing.
|
|
if kwargs['meson_version']:
|
|
self.handle_meson_version(kwargs['meson_version'], node)
|
|
else:
|
|
mesonlib.project_meson_versions[self.subproject] = mesonlib.NoProjectVersion()
|
|
|
|
self._load_option_file()
|
|
|
|
self.project_default_options = kwargs['default_options']
|
|
if self.environment.first_invocation or (self.subproject != '' and self.subproject not in self.coredata.initialized_subprojects):
|
|
if self.subproject == '':
|
|
self.coredata.optstore.initialize_from_top_level_project_call(self.project_default_options,
|
|
self.user_defined_options.cmd_line_options,
|
|
self.environment.options)
|
|
else:
|
|
self.coredata.optstore.initialize_from_subproject_call(self.subproject,
|
|
self.invoker_method_default_options,
|
|
self.project_default_options,
|
|
self.user_defined_options.cmd_line_options,
|
|
self.environment.options)
|
|
self.coredata.initialized_subprojects.add(self.subproject)
|
|
|
|
if not self.is_subproject():
|
|
# We have to activate VS before adding languages and before calling
|
|
# self.set_backend() otherwise it wouldn't be able to detect which
|
|
# vs backend version we need. But after setting default_options in case
|
|
# the project sets vs backend by default.
|
|
backend = self.coredata.optstore.get_value_for(OptionKey('backend'))
|
|
assert backend is None or isinstance(backend, str), 'for mypy'
|
|
vsenv = self.coredata.optstore.get_value_for(OptionKey('vsenv'))
|
|
assert isinstance(vsenv, bool), 'for mypy'
|
|
force_vsenv = vsenv or backend.startswith('vs')
|
|
mesonlib.setup_vsenv(force_vsenv)
|
|
self.set_backend()
|
|
|
|
if not self.is_subproject():
|
|
self.build.project_name = proj_name
|
|
self.active_projectname = proj_name
|
|
|
|
version = kwargs['version']
|
|
if isinstance(version, mesonlib.File):
|
|
FeatureNew.single_use('version from file', '0.57.0', self.subproject, location=node)
|
|
self.add_build_def_file(version)
|
|
ifname = version.absolute_path(self.environment.source_dir,
|
|
self.environment.build_dir)
|
|
try:
|
|
ver_data = Path(ifname).read_text(encoding='utf-8').split('\n')
|
|
except FileNotFoundError:
|
|
raise InterpreterException('Version file not found.')
|
|
if len(ver_data) == 2 and ver_data[1] == '':
|
|
ver_data = ver_data[0:1]
|
|
if len(ver_data) != 1:
|
|
raise InterpreterException('Version file must contain exactly one line of text.')
|
|
self.project_version = ver_data[0]
|
|
else:
|
|
self.project_version = version
|
|
|
|
if self.build.project_version is None:
|
|
self.build.project_version = self.project_version
|
|
|
|
if kwargs['license'] is None:
|
|
proj_license = ['unknown']
|
|
if kwargs['license_files']:
|
|
raise InvalidArguments('Project `license` name must be specified when `license_files` is set')
|
|
else:
|
|
proj_license = kwargs['license']
|
|
proj_license_files = []
|
|
for i in self.source_strings_to_files(kwargs['license_files']):
|
|
ifname = i.absolute_path(self.environment.source_dir,
|
|
self.environment.build_dir)
|
|
proj_license_files.append((ifname, i))
|
|
self.build.dep_manifest[proj_name] = build.DepManifest(self.project_version, proj_license,
|
|
proj_license_files, self.subproject)
|
|
|
|
for_machine = self.build.machine_map.host
|
|
if self.subproject in self.build.projects[for_machine]:
|
|
raise InvalidCode('Second call to project().')
|
|
|
|
# spdirname is the subproject_dir for this project, relative to self.subdir.
|
|
# self.subproject_dir is the subproject_dir for the main project, relative to top source dir.
|
|
spdirname = kwargs['subproject_dir']
|
|
if not isinstance(spdirname, str):
|
|
raise InterpreterException('Subproject_dir must be a string')
|
|
if path_has_root(spdirname):
|
|
raise InterpreterException('Subproject_dir must not be an absolute path.')
|
|
if spdirname.startswith('.'):
|
|
raise InterpreterException('Subproject_dir must not begin with a period.')
|
|
if '..' in spdirname:
|
|
raise InterpreterException('Subproject_dir must not contain a ".." segment.')
|
|
if not self.is_subproject():
|
|
self.subproject_dir = spdirname
|
|
self.build.subproject_dir = self.subproject_dir
|
|
|
|
# Load wrap files from this (sub)project.
|
|
subprojects_dir = os.path.join(self.subdir, spdirname)
|
|
if not self.is_subproject():
|
|
wrap_mode_s = self.coredata.optstore.get_value_for(OptionKey('wrap_mode'))
|
|
assert isinstance(wrap_mode_s, str), 'for mypy'
|
|
wrap_mode = WrapMode.from_string(wrap_mode_s)
|
|
self.environment.wrap_resolver = wrap.Resolver(self.environment.get_source_dir(), subprojects_dir, self.subproject, wrap_mode)
|
|
else:
|
|
self.environment.wrap_resolver.load_and_merge(subprojects_dir, self.subproject)
|
|
|
|
if self.cargo is None:
|
|
self.load_root_cargo_lock_file()
|
|
|
|
build_project = build.BuildProject(proj_name, self.project_version, self.subproject,
|
|
self.build.for_machine, for_machine, self.subdir)
|
|
self.build.projects[for_machine][self.subproject] = build_project
|
|
|
|
extra_args: T.List[mlog.TV_Loggable] = []
|
|
if self.is_subproject() and for_machine is MachineChoice.BUILD:
|
|
extra_args.append('(for build machine)')
|
|
mlog.log('Project name:', mlog.bold(proj_name), *extra_args)
|
|
mlog.log('Project version:', mlog.bold(self.project_version))
|
|
|
|
self.add_languages(proj_langs, True, MachineChoice.HOST)
|
|
self.add_languages(proj_langs, False, MachineChoice.BUILD)
|
|
|
|
if not self.is_subproject():
|
|
self.check_stdlibs()
|
|
|
|
@TypedArgs(
|
|
'add_languages',
|
|
var_types=STR_VARG,
|
|
kw_types=[
|
|
KwargInfo(
|
|
'native',
|
|
(bool, NoneType),
|
|
since='0.54.0',
|
|
convertor=lambda x: {None: None, True: MachineChoice.BUILD, False: MachineChoice.HOST}[x],
|
|
),
|
|
REQUIRED_KW,
|
|
],
|
|
)
|
|
def func_add_languages(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddLanguages') -> bool:
|
|
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
|
|
for_machine = kwargs['native']
|
|
|
|
langs = self._validate_languages(args[0], required, node)
|
|
|
|
if disabled:
|
|
for lang in sorted(langs, key=compilers.sort_clink):
|
|
mlog.log('Compiler for language', mlog.bold(lang), 'skipped: feature', mlog.bold(feature), 'disabled')
|
|
return False
|
|
if for_machine is not None:
|
|
for_machine = self.build.machine_map[for_machine]
|
|
return self.add_languages(langs, required, for_machine)
|
|
else:
|
|
# absent 'native' means 'both' for backwards compatibility
|
|
tv = FeatureNew.get_target_version(self.subproject)
|
|
if FeatureNew.check_version(tv, '0.54'):
|
|
mlog.warning('add_languages is missing native:, assuming languages are wanted for both host and build.',
|
|
location=node)
|
|
|
|
# If languages were removed as invalid, then return false
|
|
success = len(langs) == len(args[0])
|
|
success &= self.add_languages(langs, required, MachineChoice.HOST)
|
|
success &= self.add_languages(langs, False, MachineChoice.BUILD)
|
|
return success
|
|
|
|
def _stringify_user_arguments(self, args: T.List[TYPE_var], func_name: str) -> list[str]:
|
|
try:
|
|
return [stringifyUserArguments(i, self.subproject) for i in args]
|
|
except InvalidArguments as e:
|
|
raise InvalidArguments(f'{func_name}(): {str(e)}')
|
|
|
|
@noArgsFlattening
|
|
@TypedArgs('message', var_types=OBJ_VARG.evolve(variadic_since='0.54.0'))
|
|
def func_message(self, node: mparser.BaseNode, args: tuple[list[TYPE_var]], kwargs: TYPE_kwargs) -> None:
|
|
args_str = self._stringify_user_arguments(args[0], 'message')
|
|
self.message_impl(args_str)
|
|
|
|
def message_impl(self, args: mlog.TV_LoggableList | list[str]) -> None:
|
|
mlog.log(mlog.bold('Message:'), *args)
|
|
|
|
@noArgsFlattening
|
|
@FeatureNew('summary', '0.53.0')
|
|
@TypedArgs(
|
|
'summary',
|
|
pos_types=[PosArgInfo((str, dict))],
|
|
opt_types=[OBJ_OARG],
|
|
kw_types=[
|
|
KwargInfo('section', str, default=''),
|
|
KwargInfo('bool_yn', bool, default=False),
|
|
KwargInfo('list_sep', (str, NoneType), since='0.54.0')
|
|
],
|
|
)
|
|
def func_summary(self, node: mparser.BaseNode, args: T.Tuple[T.Union[str, T.Dict[str, T.Any]], T.Optional[T.Any]],
|
|
kwargs: 'kwtypes.Summary') -> None:
|
|
if args[1] is None:
|
|
if not isinstance(args[0], dict):
|
|
raise InterpreterException('Summary first argument must be dictionary.')
|
|
values = args[0]
|
|
else:
|
|
if not isinstance(args[0], str):
|
|
raise InterpreterException('Summary first argument must be string.')
|
|
values = {args[0]: args[1]}
|
|
self.summary_impl(values=values, **kwargs)
|
|
|
|
def summary_impl(self, section: str, values: dict[str, mlog.TV_Loggable | mlog.TV_LoggableList], bool_yn: bool, list_sep: str | None) -> None:
|
|
summary = self.current_build_project().summary
|
|
for k, v in values.items():
|
|
formatted_values: mlog.TV_LoggableList = []
|
|
for i in listify(v):
|
|
if isinstance(i, bool):
|
|
if bool_yn:
|
|
formatted_values.append(mlog.green('YES') if i else mlog.red('NO'))
|
|
else:
|
|
formatted_values.append('true' if i else 'false')
|
|
elif isinstance(i, (str, int)):
|
|
formatted_values.append(str(i))
|
|
elif isinstance(i, (ExternalProgram, Dependency)):
|
|
FeatureNew.single_use('dependency or external program in summary', '0.57.0', self.subproject)
|
|
formatted_values.append(i.summary_value())
|
|
elif isinstance(i, Disabler):
|
|
FeatureNew.single_use('disabler in summary', '0.64.0', self.subproject)
|
|
formatted_values.append(mlog.red('NO'))
|
|
elif isinstance(i, Feature):
|
|
FeatureNew.single_use('feature option in summary', '0.58.0', self.subproject)
|
|
formatted_values.append(str(i))
|
|
else:
|
|
m = 'Summary value in section {!r}, key {!r}, must be string, integer, boolean, dependency, disabler, or external program'
|
|
raise InterpreterException(m.format(section, k))
|
|
summary.add_value(section, k, formatted_values, list_sep)
|
|
|
|
def _print_subprojects(self, for_machine: MachineChoice) -> None:
|
|
# Add automatic 'Subprojects' section in main project.
|
|
all_subprojects: dict[str, mlog.TV_Loggable | mlog.TV_LoggableList] = {}
|
|
for name, subp in sorted(self.subprojects[for_machine].items()):
|
|
value: mlog.TV_LoggableList = [subp.found()]
|
|
if subp.disabled_feature:
|
|
value += [f'Feature {subp.disabled_feature!r} disabled']
|
|
elif subp.exception:
|
|
value += [str(subp.exception)]
|
|
elif subp.warnings > 0:
|
|
value += [f'{subp.warnings} warnings']
|
|
if subp.callstack:
|
|
stack = self.format_subproject_stack(subp.callstack)
|
|
value += [f'(from {stack})']
|
|
all_subprojects[name] = value
|
|
if all_subprojects:
|
|
self.summary_impl(f'Subprojects (for {for_machine.get_lower_case_name()} machine)', all_subprojects,
|
|
bool_yn=True, list_sep=' ')
|
|
|
|
def _print_summary(self) -> None:
|
|
self._print_subprojects(MachineChoice.HOST)
|
|
if self.environment.is_cross_build():
|
|
self._print_subprojects(MachineChoice.BUILD)
|
|
# Add automatic section with all user defined options
|
|
if self.user_defined_options:
|
|
values: dict[str, mlog.TV_Loggable | mlog.TV_LoggableList] = {}
|
|
if self.user_defined_options.cross_file:
|
|
values['Cross files'] = self.user_defined_options.cross_file
|
|
if self.user_defined_options.native_file:
|
|
values['Native files'] = self.user_defined_options.native_file
|
|
|
|
sorted_options = sorted(self.user_defined_options.cmd_line_options.items(), key=lambda x: x[0].name)
|
|
values.update({str(k): v for k, v in sorted_options})
|
|
if values:
|
|
self.summary_impl('User defined options', values, bool_yn=False, list_sep=None)
|
|
# Print all summaries, main project last.
|
|
mlog.log('') # newline
|
|
for for_machine in MachineChoice:
|
|
for name, project in self.build.projects[for_machine].items():
|
|
if name != mesonlib.ROOT_SUBPROJECT:
|
|
assert self.subprojects[for_machine][name].found()
|
|
project.dump_summary()
|
|
self.current_build_project().dump_summary()
|
|
|
|
@noArgsFlattening
|
|
@FeatureNew('warning', '0.44.0')
|
|
@TypedArgs('warning', var_types=OBJ_VARG.evolve(variadic_since='0.54.0'))
|
|
def func_warning(self, node: mparser.BaseNode, args: tuple[list[TYPE_var]], kwargs: TYPE_kwargs) -> None:
|
|
args_str = self._stringify_user_arguments(args[0], 'warning')
|
|
mlog.warning(*args_str, location=node)
|
|
|
|
@noArgsFlattening
|
|
@TypedArgs('error', var_types=OBJ_VARG.evolve(variadic_since='0.54.0'))
|
|
def func_error(self, node: mparser.BaseNode, args: tuple[list[TYPE_var]], kwargs: TYPE_kwargs) -> T.NoReturn:
|
|
args_str = self._stringify_user_arguments(args[0], 'error')
|
|
raise InterpreterException('Problem encountered: ' + ' '.join(args_str))
|
|
|
|
@noArgsFlattening
|
|
@FeatureNew('debug', '0.63.0')
|
|
@TypedArgs('debug', var_types=OBJ_VARG)
|
|
def func_debug(self, node: mparser.BaseNode, args: tuple[list[TYPE_var]], kwargs: TYPE_kwargs) -> None:
|
|
args_str = self._stringify_user_arguments(args[0], 'debug')
|
|
mlog.debug('Debug:', *args_str)
|
|
|
|
@TypedArgs('exception')
|
|
def func_exception(self, node: mparser.BaseNode, args: tuple[()], kwargs: TYPE_kwargs) -> T.NoReturn:
|
|
raise RuntimeError('unit test traceback :)')
|
|
|
|
@TypedArgs(
|
|
'expect_error',
|
|
pos_types=[STR_PARG],
|
|
kw_types=[
|
|
KwargInfo('how', str, default='literal', validator=in_set_validator({'literal', 're'})),
|
|
],
|
|
)
|
|
def func_expect_error(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: kwtypes.FuncExpectError) -> ContextManagerObject:
|
|
class ExpectErrorObject(ContextManagerObject):
|
|
def __init__(self, msg: str, how: str, subproject: SubProject) -> None:
|
|
super().__init__(subproject)
|
|
self.old_stdout = sys.stdout
|
|
sys.stdout = self.new_stdout = io.StringIO()
|
|
sys.stdout.colorize_console = getattr(self.old_stdout, 'colorize_console', None) # type: ignore[attr-defined]
|
|
self.msg = msg
|
|
self.how = how
|
|
|
|
def __exit__(self, exc_type: type[BaseException], exc_val: BaseException, exc_tb: types.TracebackType) -> bool:
|
|
sys.stdout = self.old_stdout
|
|
for l in self.new_stdout.getvalue().splitlines():
|
|
if 'ERROR:' in l:
|
|
print(l.replace('ERROR', 'ERROR (msbuild proof)'))
|
|
else:
|
|
print(l)
|
|
if exc_val is None:
|
|
raise InterpreterException('Expecting an error but code block succeeded')
|
|
if isinstance(exc_val, mesonlib.MesonException):
|
|
msg = str(exc_val)
|
|
if (self.how == 'literal' and self.msg != msg) or \
|
|
(self.how == 're' and not re.match(self.msg, msg)):
|
|
raise InterpreterException(f'Expecting error {self.msg!r} but got {msg!r}')
|
|
return True
|
|
return False
|
|
return ExpectErrorObject(args[0], kwargs['how'], self.subproject)
|
|
|
|
def add_languages(self, args: T.List[Language], required: bool, for_machine: MachineChoice) -> bool:
|
|
success = self.add_languages_for(args, required, for_machine)
|
|
self._redetect_machines()
|
|
return success
|
|
|
|
def should_skip_sanity_check(self, for_machine: MachineChoice) -> bool:
|
|
should = self.environment.properties.host.get('skip_sanity_check', False)
|
|
if not isinstance(should, bool):
|
|
raise InterpreterException('Option skip_sanity_check must be a boolean.')
|
|
if for_machine != MachineChoice.HOST and not should:
|
|
return False
|
|
if not self.environment.is_cross_build() and not should:
|
|
return False
|
|
return should
|
|
|
|
def add_languages_for(self, args: T.List[Language], required: bool, for_machine: MachineChoice) -> bool:
|
|
langs = set(self.compilers[for_machine])
|
|
langs.update(args)
|
|
|
|
# Some languages are added only as implementation details of other
|
|
# languages. When that happens, args is modified in place, so make
|
|
# a copy.
|
|
args = list(args)
|
|
|
|
# We don't want to add those languages to the project enabled languages,
|
|
# as they are implementation details, not something the user explicitly
|
|
# asked for, and we don't want to be forced to keep them forever
|
|
# if the implementation changes.
|
|
internal: T.Set[Language] = set()
|
|
|
|
if 'vala' in langs and 'c' not in langs:
|
|
FeatureNew.single_use('Adding Vala language without C', '0.59.0', self.subproject, location=self.current_node)
|
|
args.append('c')
|
|
|
|
# We need to initialize the default cython language here, as we need it
|
|
# for sanity checking. We may need to initialize a different language later
|
|
# if a target has the `cython_language` overridden. We only want to
|
|
# initialize a default if cython is a new language, not if it's an
|
|
# existing one from `self.compilers`.
|
|
if 'cython' in args:
|
|
_lang = self.coredata.optstore.get_pending_value(OptionKey('cython_language', None, for_machine), 'c')
|
|
assert isinstance(_lang, str), 'for mypy'
|
|
cython_lang = T.cast('Language', _lang)
|
|
if cython_lang not in langs:
|
|
FeatureNew.single_use(f'Adding Cython language without {cython_lang}', '1.11',
|
|
self.subproject, location=self.current_node)
|
|
args.append(cython_lang)
|
|
internal.add(cython_lang)
|
|
|
|
if 'cuda' in args and 'cpp' not in langs:
|
|
# Cuda requires C++, ensure we initialize that compiler, but don't
|
|
# add that to the project compiler list
|
|
# No FeatureNew is required here because we always did this, we just
|
|
# did it wrong.
|
|
args.append('cpp')
|
|
internal.add('cpp')
|
|
|
|
if 'nasm' in langs:
|
|
FeatureNew.single_use('Adding NASM language', '0.64.0', self.subproject, location=self.current_node)
|
|
|
|
success = True
|
|
for lang in sorted(args, key=compilers.sort_clink):
|
|
if lang in self.compilers[for_machine]:
|
|
continue
|
|
machine_name = for_machine.get_lower_case_name()
|
|
comp = self.coredata.compilers[for_machine].get(lang)
|
|
if not comp:
|
|
try:
|
|
skip_sanity_check = self.should_skip_sanity_check(for_machine)
|
|
if skip_sanity_check:
|
|
mlog.log('Cross compiler sanity tests disabled via the cross file.', once=True)
|
|
comp = compilers.detect_compiler_for(self.environment, lang, for_machine, skip_sanity_check, self.subproject)
|
|
if comp is None:
|
|
raise InvalidArguments(f'Tried to use unknown language "{lang}".')
|
|
except mesonlib.MesonException:
|
|
if not required:
|
|
mlog.log('Compiler for language',
|
|
mlog.bold(lang), 'for the', machine_name,
|
|
'machine not found.')
|
|
success = False
|
|
continue
|
|
else:
|
|
raise
|
|
if lang == 'cuda' and hasattr(self.backend, 'allow_thin_archives'):
|
|
# see NinjaBackend.__init__() why we need to disable thin archives for cuda
|
|
mlog.debug('added cuda as language, disabling thin archives for {}, since nvcc/nvlink cannot handle thin archives natively'.format(for_machine))
|
|
self.backend.allow_thin_archives[for_machine] = False
|
|
else:
|
|
# update new values from commandline, if it applies
|
|
self.coredata.process_compiler_options(lang, comp, self.subproject)
|
|
|
|
if for_machine == MachineChoice.HOST or self.environment.is_cross_build():
|
|
logger_fun = mlog.log
|
|
else:
|
|
logger_fun = mlog.debug
|
|
logger_fun(comp.get_display_language(), 'compiler for the', machine_name, 'machine:',
|
|
mlog.bold(' '.join(comp.get_exelist())), comp.get_version_string())
|
|
if comp.linker is not None:
|
|
logger_fun(comp.get_display_language(), 'linker for the', machine_name, 'machine:',
|
|
mlog.bold(' '.join(comp.linker.get_exelist())), comp.linker.id, comp.linker.version)
|
|
self.build.ensure_static_linker(comp)
|
|
if lang not in internal:
|
|
self.compilers[for_machine][lang] = comp
|
|
|
|
return success
|
|
|
|
def program_from_file_for(self, for_machine: MachineChoice, prognames: T.List[mesonlib.FileOrString]
|
|
) -> T.Optional[ExternalProgram]:
|
|
for p in prognames:
|
|
if isinstance(p, mesonlib.File):
|
|
continue # Always points to a local (i.e. self generated) file.
|
|
if not isinstance(p, str):
|
|
raise InterpreterException('Executable name must be a string')
|
|
prog = ExternalProgram.from_bin_list(self.environment, for_machine, p)
|
|
# if the machine file specified something, it may be a regular
|
|
# not-found program but we still want to return that
|
|
if not isinstance(prog, NonExistingExternalProgram):
|
|
return prog
|
|
return None
|
|
|
|
def program_from_system(self, args: T.List[mesonlib.FileOrString], search_dirs: T.Optional[T.List[str]],
|
|
extra_info: T.List[mlog.TV_Loggable]) -> T.Optional[ExternalProgram]:
|
|
# Search for scripts relative to current subdir.
|
|
# Do not cache found programs because find_program('foobar')
|
|
# might give different results when run from different source dirs.
|
|
source_dir = os.path.join(self.environment.get_source_dir(), self.subdir)
|
|
for exename in args:
|
|
if isinstance(exename, mesonlib.File):
|
|
if exename.is_built:
|
|
search_dir = os.path.join(self.environment.get_build_dir(),
|
|
exename.subdir)
|
|
else:
|
|
search_dir = os.path.join(self.environment.get_source_dir(),
|
|
exename.subdir)
|
|
exename = exename.fname
|
|
search_dirs = [search_dir]
|
|
elif isinstance(exename, str):
|
|
if search_dirs:
|
|
search_dirs = [source_dir] + search_dirs
|
|
else:
|
|
search_dirs = [source_dir]
|
|
else:
|
|
raise InvalidArguments(f'find_program only accepts strings and files, not {exename!r}')
|
|
extprog = ExternalProgram(exename, search_dirs=search_dirs, silent=True)
|
|
if extprog.found():
|
|
extra_info.append(f"({' '.join(extprog.get_command())})")
|
|
return extprog
|
|
return None
|
|
|
|
def program_from_overrides(self, command_names: T.List[mesonlib.FileOrString],
|
|
for_machine: MachineChoice,
|
|
extra_info: T.List['mlog.TV_Loggable']
|
|
) -> T.Optional[Program]:
|
|
for name in command_names:
|
|
if not isinstance(name, str):
|
|
continue
|
|
if name in self.build.find_overrides[for_machine]:
|
|
exe = self.build.find_overrides[for_machine][name]
|
|
extra_info.append(mlog.blue('(overridden)'))
|
|
return exe
|
|
return None
|
|
|
|
def store_name_lookups(self, command_names: T.List[mesonlib.FileOrString], for_machine: MachineChoice) -> None:
|
|
for name in command_names:
|
|
if isinstance(name, str):
|
|
self.build.searched_programs[for_machine].add(name)
|
|
|
|
def add_find_program_override(self, name: str, exe: Program, for_machine: MachineChoice) -> None:
|
|
if name in self.build.searched_programs[for_machine]:
|
|
raise InterpreterException(f'Tried to override finding of executable "{name}" which has already been found.')
|
|
previous = self.build.find_overrides[for_machine].get(name)
|
|
if previous is not None:
|
|
# For native builds, both machines map to MachineChoice.HOST. Allow
|
|
# overriding the same program with the same executable, so that it is
|
|
# possible to call meson.override_find_program consecutively with
|
|
# `native: true` and `native: false`.
|
|
if previous == exe:
|
|
FeatureNew.single_use('Overriding the same program multiple times (with the same executable)',
|
|
'1.13.0', self.subproject)
|
|
return
|
|
raise InterpreterException(f'Tried to override executable "{name}" which has already been overridden.')
|
|
self.build.find_overrides[for_machine][name] = exe
|
|
if name == 'pkg-config' and isinstance(exe, ExternalProgram):
|
|
from ..dependencies.pkgconfig import PkgConfigInterface
|
|
PkgConfigInterface.set_program_override(exe, for_machine)
|
|
|
|
def notfound_program(self, args: T.List[mesonlib.FileOrString]) -> ExternalProgram:
|
|
return NonExistingExternalProgram(' '.join(
|
|
[a if isinstance(a, str) else a.absolute_path(self.environment.source_dir, self.environment.build_dir)
|
|
for a in args]))
|
|
|
|
# TODO update modules to always pass `for_machine`. It is bad-form to assume
|
|
# the host machine.
|
|
def find_program_impl(self, args: T.List[mesonlib.FileOrString],
|
|
for_machine: MachineChoice = MachineChoice.HOST,
|
|
default_options: T.Optional[OptionDict] = None,
|
|
required: bool = True, silent: bool = True,
|
|
wanted: T.Union[str, T.List[str]] = '',
|
|
search_dirs: T.Optional[T.List[str]] = None,
|
|
version_arg: T.Optional[str] = '',
|
|
version_func: T.Optional[ProgramVersionFunc] = None
|
|
) -> Program:
|
|
args = mesonlib.listify(args)
|
|
|
|
extra_info: T.List[mlog.TV_Loggable] = []
|
|
progobj = self.program_lookup(args, for_machine, default_options, required, search_dirs, wanted, version_arg, version_func, extra_info)
|
|
if progobj is None or not self.check_program_version(progobj, wanted, version_func, for_machine, extra_info):
|
|
progobj = self.notfound_program(args)
|
|
|
|
if isinstance(progobj, ExternalProgram) and not progobj.found():
|
|
if not silent:
|
|
mlog.log('Program', mlog.bold(progobj.get_name()), 'found:', mlog.red('NO'), *extra_info)
|
|
if required:
|
|
m = 'Program {!r} not found or not executable'
|
|
raise InterpreterException(m.format(progobj.get_name()))
|
|
return progobj
|
|
|
|
# Only store successful lookups
|
|
self.store_name_lookups(args, for_machine)
|
|
if not silent:
|
|
mlog.log('Program', mlog.bold(progobj.name), 'found:', mlog.green('YES'), *extra_info)
|
|
return progobj
|
|
|
|
def program_lookup(self, args: T.List[mesonlib.FileOrString], for_machine: MachineChoice,
|
|
default_options: T.Optional[OptionDict],
|
|
required: bool,
|
|
search_dirs: T.Optional[T.List[str]],
|
|
wanted: T.Union[str, T.List[str]],
|
|
version_arg: T.Optional[str],
|
|
version_func: T.Optional[ProgramVersionFunc],
|
|
extra_info: T.List[mlog.TV_Loggable]
|
|
) -> T.Optional[Program]:
|
|
progobj = self.program_from_overrides(args, for_machine, extra_info)
|
|
if progobj:
|
|
return progobj
|
|
|
|
if args[0] == 'meson':
|
|
# Override find_program('meson') to return what we were invoked with
|
|
return ExternalProgram('meson', self.environment.get_build_command(), silent=True)
|
|
|
|
wrap_mode_s = self.coredata.optstore.get_value_for(OptionKey('wrap_mode'))
|
|
force_fallback_for = self.coredata.optstore.get_value_for(OptionKey('force_fallback_for'))
|
|
assert isinstance(wrap_mode_s, str), 'for mypy'
|
|
assert isinstance(force_fallback_for, list), 'for mypy'
|
|
wrap_mode = WrapMode.from_string(wrap_mode_s)
|
|
|
|
fallback: SubProject | None = \
|
|
self.environment.wrap_resolver.find_program_provider(args) \
|
|
if self.environment.wrap_resolver is not None \
|
|
else None
|
|
if fallback and (wrap_mode == WrapMode.forcefallback or fallback in force_fallback_for):
|
|
return self.find_program_fallback(fallback, args, for_machine, default_options, required, extra_info)
|
|
|
|
progobj = None
|
|
version_fail_info: T.List[mlog.TV_Loggable] = []
|
|
# A machine-file entry is authoritative for its name, even if its
|
|
# version is unsuitable. Only unconfigured names may use system lookup.
|
|
for arg in args:
|
|
candidate_extra: T.List[mlog.TV_Loggable] = []
|
|
candidate = self.program_from_file_for(for_machine, [arg])
|
|
if candidate is None:
|
|
candidate = self.program_from_system([arg], search_dirs, candidate_extra)
|
|
if candidate is None and isinstance(arg, str) and arg.endswith('python3'):
|
|
candidate = ExternalProgram('python3', mesonlib.python_command, silent=True)
|
|
if candidate is None or not candidate.found():
|
|
continue
|
|
if version_arg:
|
|
candidate.version_arg = version_arg
|
|
ver_info: T.List[mlog.TV_Loggable] = []
|
|
if self.check_program_version(candidate, wanted, version_func, for_machine, ver_info):
|
|
extra_info.extend(candidate_extra)
|
|
extra_info.extend(ver_info)
|
|
progobj = candidate
|
|
break
|
|
version_fail_info.extend(candidate_extra)
|
|
version_fail_info.extend(ver_info)
|
|
else:
|
|
extra_info.extend(version_fail_info)
|
|
|
|
if progobj is None and fallback and required:
|
|
progobj = self.notfound_program(args)
|
|
mlog.log('Program', mlog.bold(progobj.get_name()), 'found:', mlog.red('NO'), *extra_info)
|
|
extra_info.clear()
|
|
progobj = self.find_program_fallback(fallback, args, for_machine, default_options, required, extra_info)
|
|
|
|
return progobj
|
|
|
|
def check_program_version(self, progobj: Program,
|
|
wanted: T.Union[str, T.List[str]],
|
|
version_func: T.Optional[ProgramVersionFunc],
|
|
for_machine: MachineChoice,
|
|
extra_info: T.List[mlog.TV_Loggable]) -> bool:
|
|
if wanted:
|
|
if version_func:
|
|
version = version_func(progobj)
|
|
elif isinstance(progobj, build.Executable):
|
|
if progobj.subproject:
|
|
interp = self.subprojects[for_machine][progobj.subproject].held_object
|
|
else:
|
|
interp = self
|
|
assert isinstance(interp, Interpreter), 'for mypy'
|
|
version = interp.project_version
|
|
else:
|
|
version = progobj.get_version(self)
|
|
is_found, not_found, _ = mesonlib.version_compare_many(version, wanted)
|
|
if not is_found:
|
|
extra_info[:0] = ['found', mlog.normal_cyan(version), 'but need:',
|
|
mlog.bold(', '.join([f"'{e}'" for e in not_found]))]
|
|
return False
|
|
extra_info.insert(0, mlog.normal_cyan(version))
|
|
return True
|
|
|
|
def find_program_fallback(self, fallback: SubProject, args: T.List[mesonlib.FileOrString],
|
|
for_machine: MachineChoice,
|
|
default_options: OptionDict,
|
|
required: bool, extra_info: T.List[mlog.TV_Loggable],
|
|
) -> T.Optional[Program]:
|
|
mlog.log('Fallback to subproject', mlog.bold(fallback), 'which provides program',
|
|
mlog.bold(' '.join(str(a) for a in args)))
|
|
sp_kwargs: kwtypes.DoSubproject = {
|
|
'required': required,
|
|
'default_options': default_options or {},
|
|
'version': [],
|
|
'cmake_options': [],
|
|
'options': None,
|
|
'for_machine': for_machine,
|
|
}
|
|
self.do_subproject(fallback, sp_kwargs)
|
|
return self.program_from_overrides(args, for_machine, extra_info)
|
|
|
|
@TypedArgs(
|
|
'find_program',
|
|
var_types=STR_FILE_VARG.evolve(min_args=1),
|
|
kw_types=[
|
|
DISABLER_KW.evolve(since='0.49.0'),
|
|
NATIVE_KW,
|
|
REQUIRED_KW,
|
|
KwargInfo('dirs', ContainerTypeInfo(list, str), default=[], listify=True, since='0.53.0'),
|
|
KwargInfo('version', ContainerTypeInfo(list, str), default=[], listify=True, since='0.52.0'),
|
|
KwargInfo('version_argument', str, default='', since='1.5.0'),
|
|
DEFAULT_OPTIONS.evolve(since='1.3.0')
|
|
],
|
|
)
|
|
@disablerIfNotFound
|
|
@apply_machine_map
|
|
def func_find_program(self, node: mparser.BaseNode, args: T.Tuple[T.List[mesonlib.FileOrString]],
|
|
kwargs: 'kwtypes.FindProgram',
|
|
) -> Program:
|
|
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
|
|
if disabled:
|
|
mlog.log('Program', mlog.bold(' '.join(str(a) for a in args[0])), 'skipped: feature', mlog.bold(feature), 'disabled')
|
|
return self.notfound_program(args[0])
|
|
|
|
search_dirs = extract_search_dirs(kwargs)
|
|
default_options = kwargs['default_options']
|
|
return self.find_program_impl(args[0], kwargs['native'], default_options=default_options, required=required,
|
|
silent=False, wanted=kwargs['version'], version_arg=kwargs['version_argument'],
|
|
search_dirs=search_dirs)
|
|
|
|
# When adding kwargs, please check if they make sense in dependencies.get_dep_identifier()
|
|
@TypedArgs(
|
|
'dependency',
|
|
var_types=STR_VARG_1,
|
|
kw_types=DEPENDENCY_KWS
|
|
)
|
|
@disablerIfNotFound
|
|
@apply_machine_map
|
|
def func_dependency(self, node: mparser.BaseNode, args: T.Tuple[T.List[str]], kwargs: kwtypes.FuncDependency) -> Dependency:
|
|
# Replace '' by empty list of names
|
|
names = [n for n in args[0] if n]
|
|
if len(names) > 1:
|
|
FeatureNew('dependency with more than one name', '0.60.0').use(self.subproject)
|
|
default_options = kwargs.get('default_options')
|
|
for_machine = kwargs['native']
|
|
df = DependencyFallbacksHolder(self, names, for_machine, kwargs['allow_fallback'], default_options)
|
|
df.set_fallback(kwargs['fallback'])
|
|
not_found_message = kwargs['not_found_message']
|
|
|
|
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
|
|
if disabled:
|
|
name = names[0]
|
|
if kwargs['modules']:
|
|
name = name + '(modules: {})'.format(', '.join(kwargs['modules']))
|
|
mlog.log('Dependency', mlog.bold(name),
|
|
'for', mlog.bold(for_machine.get_lower_case_name()), 'machine',
|
|
'skipped: feature', mlog.bold(feature), 'disabled')
|
|
return dependencies.NotFoundDependency(names[0], self.environment)
|
|
|
|
nkwargs = T.cast('dependencies.base.DependencyObjectKWs', kwargs.copy())
|
|
nkwargs['required'] = required # to replace a possible Feature with a bool
|
|
|
|
try:
|
|
d = df.lookup(nkwargs)
|
|
except Exception:
|
|
if not_found_message:
|
|
self.message_impl([not_found_message])
|
|
raise
|
|
assert isinstance(d, Dependency), 'for mypy'
|
|
if not d.found() and not_found_message:
|
|
self.message_impl([not_found_message])
|
|
# Ensure the correct include type
|
|
if kwargs['include_type'] != 'preserve':
|
|
wanted = kwargs['include_type']
|
|
actual = d.get_include_type()
|
|
if wanted != actual:
|
|
mlog.debug(f'Current include type of {args[0]} is {actual}. Converting to requested {wanted}')
|
|
d = d.generate_system_dependency(wanted)
|
|
if d.feature_since is not None:
|
|
version, extra_msg = d.feature_since
|
|
FeatureNew.single_use(f'dep {d.name!r} custom lookup', version, self.subproject, extra_msg, node)
|
|
for f in d.featurechecks:
|
|
f.use(self.subproject, node)
|
|
return d
|
|
|
|
@FeatureNew('default', '1.12.0')
|
|
@TypedArgs('default')
|
|
def func_default(self, node: mparser.BaseNode, args: list[TYPE_var], kwargs: TYPE_kwargs) -> DefaultObject:
|
|
return DefaultObject()
|
|
|
|
@FeatureNew('disabler', '0.44.0')
|
|
@TypedArgs('disabler')
|
|
def func_disabler(self, node: mparser.BaseNode, args: list[TYPE_var], kwargs: TYPE_kwargs) -> Disabler:
|
|
return Disabler()
|
|
|
|
@TypedArgs(
|
|
'executable',
|
|
pos_types=[STR_PARG],
|
|
var_types=SRC_VARG,
|
|
kw_types=EXECUTABLE_KWS,
|
|
)
|
|
def func_executable(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.Executable) -> T.Union[build.Executable, build.SharedLibrary]:
|
|
return self.build_target(node, args, kwargs, build.Executable)
|
|
|
|
@TypedArgs(
|
|
'static_library',
|
|
pos_types=[STR_PARG],
|
|
var_types=SRC_VARG,
|
|
kw_types=STATIC_LIB_KWS,
|
|
)
|
|
def func_static_lib(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.StaticLibrary) -> build.StaticLibrary:
|
|
return self.build_target(node, args, kwargs, build.StaticLibrary)
|
|
|
|
@TypedArgs(
|
|
'shared_library',
|
|
pos_types=[STR_PARG],
|
|
var_types=SRC_VARG,
|
|
kw_types=SHARED_LIB_KWS,
|
|
)
|
|
def func_shared_lib(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.SharedLibrary) -> build.SharedLibrary:
|
|
return self.build_target(node, args, kwargs, build.SharedLibrary)
|
|
|
|
@TypedArgs(
|
|
'both_libraries',
|
|
pos_types=[STR_PARG],
|
|
var_types=SRC_VARG,
|
|
kw_types=LIBRARY_KWS
|
|
)
|
|
def func_both_lib(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.Library) -> build.BothLibraries:
|
|
return self.build_both_libraries(node, args, kwargs)
|
|
|
|
@FeatureNew('shared_module', '0.37.0')
|
|
@TypedArgs(
|
|
'shared_module',
|
|
pos_types=[STR_PARG],
|
|
var_types=SRC_VARG,
|
|
kw_types=SHARED_MOD_KWS
|
|
)
|
|
def func_shared_module(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.SharedModule) -> build.SharedModule:
|
|
return self.build_target(node, args, kwargs, build.SharedModule)
|
|
|
|
@TypedArgs(
|
|
'library',
|
|
pos_types=[STR_PARG],
|
|
var_types=SRC_VARG,
|
|
kw_types=LIBRARY_KWS
|
|
)
|
|
def func_library(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.Library) -> build.StaticLibrary | build.SharedLibrary | build.BothLibraries:
|
|
return self.build_library(node, args, kwargs)
|
|
|
|
@TypedArgs(
|
|
'jar',
|
|
pos_types=[STR_PARG],
|
|
var_types=VarArgInfo((str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex, build.GeneratedList, build.ExtractedObjects, build.BuildTarget)),
|
|
kw_types=JAR_KWS,
|
|
)
|
|
def func_jar(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, T.List[str | build.TargetSources]],
|
|
kwargs: kwtypes.Jar) -> build.Jar:
|
|
return self.build_target(node, T.cast('tuple[str, SourcesVarargsType]', args), kwargs, build.Jar)
|
|
|
|
@TypedArgs(
|
|
'build_target',
|
|
pos_types=[STR_PARG],
|
|
var_types=SRC_VARG,
|
|
kw_types=BUILD_TARGET_KWS,
|
|
)
|
|
def func_build_target(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.BuildTargetFunc,
|
|
) -> T.Union[build.Executable, build.StaticLibrary, build.SharedLibrary,
|
|
build.SharedModule, build.BothLibraries, build.Jar]:
|
|
target_type = kwargs['target_type']
|
|
|
|
if target_type == 'executable':
|
|
return self.build_target(node, args, kwargs, build.Executable)
|
|
elif target_type == 'shared_library':
|
|
return self.build_target(node, args, kwargs, build.SharedLibrary)
|
|
elif target_type == 'shared_module':
|
|
return self.build_target(node, args, kwargs, build.SharedModule)
|
|
elif target_type == 'static_library':
|
|
return self.build_target(node, args, kwargs, build.StaticLibrary)
|
|
elif target_type == 'both_libraries':
|
|
return self.build_both_libraries(node, args, kwargs)
|
|
elif target_type == 'library':
|
|
return self.build_library(node, args, kwargs)
|
|
# We can't avoid the cast here because of the mis-matched sources types
|
|
# between Jar and BuildTarget types
|
|
return self.build_target(node, args, T.cast('kwtypes.Jar', kwargs), build.Jar)
|
|
|
|
@TypedArgs(
|
|
'vcs_tag',
|
|
kw_types=[
|
|
CT_INPUT_KW.evolve(required=True),
|
|
MULTI_OUTPUT_KW,
|
|
# Cannot use the COMMAND_KW because command is allowed to be empty
|
|
KwargInfo(
|
|
'command',
|
|
ContainerTypeInfo(list, (str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, Program, mesonlib.File)),
|
|
listify=True,
|
|
default=[],
|
|
),
|
|
KwargInfo('fallback', (str, NoneType)),
|
|
KwargInfo('replace_string', str, default='@VCS_TAG@'),
|
|
INSTALL_KW.evolve(since='1.7.0'),
|
|
INSTALL_DIR_KW.evolve(since='1.7.0'),
|
|
INSTALL_TAG_KW.evolve(since='1.7.0'),
|
|
INSTALL_MODE_KW.evolve(since='1.7.0'),
|
|
],
|
|
)
|
|
@apply_machine_map
|
|
def func_vcs_tag(self, node: mparser.BaseNode, args: T.List['TYPE_var'], kwargs: 'kwtypes.VcsTag') -> build.CustomTarget:
|
|
if kwargs['fallback'] is None:
|
|
FeatureNew.single_use('Optional fallback in vcs_tag', '0.41.0', self.subproject, location=node)
|
|
fallback = kwargs['fallback'] or self.project_version
|
|
replace_string = kwargs['replace_string']
|
|
regex_selector = '(.*)' # default regex selector for custom command: use complete output
|
|
vcs_cmd = kwargs['command']
|
|
source_dir = os.path.normpath(os.path.join(self.environment.get_source_dir(), self.subdir))
|
|
if vcs_cmd:
|
|
if isinstance(vcs_cmd[0], (str, mesonlib.File)):
|
|
if isinstance(vcs_cmd[0], mesonlib.File):
|
|
FeatureNew.single_use('vcs_tag with file as the first argument', '0.62.0', self.subproject, location=node)
|
|
maincmd = self.find_program_impl([vcs_cmd[0]], required=False)
|
|
if maincmd.found():
|
|
vcs_cmd[0] = maincmd
|
|
else:
|
|
FeatureNew.single_use('vcs_tag with custom_tgt, program, or exe as the first argument', '0.63.0', self.subproject, location=node)
|
|
else:
|
|
vcs = mesonlib.detect_vcs(source_dir)
|
|
if vcs:
|
|
mlog.log('Found {} repository at {}'.format(vcs.name, vcs.wc_dir))
|
|
vcs_cmd = vcs.get_rev
|
|
regex_selector = vcs.rev_regex
|
|
else:
|
|
vcs_cmd = [' '] # executing this cmd will fail in vcstagger.py and force to use the fallback string
|
|
# vcstagger.py parameters: infile, outfile, fallback, source_dir, replace_string, regex_selector, command...
|
|
|
|
self._validate_custom_target_outputs(len(kwargs['input']) > 1, kwargs['output'], "vcs_tag")
|
|
|
|
cmd = self.environment.get_build_command() + \
|
|
['--internal',
|
|
'vcstagger',
|
|
'@INPUT0@',
|
|
'@OUTPUT0@',
|
|
fallback,
|
|
source_dir,
|
|
replace_string,
|
|
regex_selector] + vcs_cmd
|
|
|
|
install = kwargs['install']
|
|
install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
|
|
install_dir: T.List[T.Union[str, Literal[False]]] = [] if kwargs['install_dir'] is None else [kwargs['install_dir']]
|
|
install_tag: T.List[T.Optional[str]] = [] if kwargs['install_tag'] is None else [kwargs['install_tag']]
|
|
if install and not install_dir:
|
|
raise InvalidArguments('vcs_tag: "install_dir" keyword argument must be set when "install" is true.')
|
|
|
|
tg = build.CustomTarget(
|
|
kwargs['output'][0],
|
|
self.subdir,
|
|
self.environment,
|
|
cmd,
|
|
self.source_strings_to_files(kwargs['input']),
|
|
kwargs['output'],
|
|
self.current_build_project(),
|
|
build_by_default=True,
|
|
build_always_stale=True,
|
|
install=install,
|
|
install_dir=install_dir,
|
|
install_mode=install_mode,
|
|
install_tag=install_tag,
|
|
)
|
|
self.add_target(tg.name, tg)
|
|
return tg
|
|
|
|
@FeatureNew('subdir_done', '0.46.0')
|
|
@TypedArgs('subdir_done')
|
|
def func_subdir_done(self, node: mparser.BaseNode, args: TYPE_var, kwargs: TYPE_kwargs) -> T.NoReturn:
|
|
raise SubdirDoneRequest()
|
|
|
|
def _validate_custom_target_outputs(self, has_multi_in: bool, outputs: T.Iterable[str], name: str) -> None:
|
|
"""Checks for additional invalid values in a custom_target output.
|
|
|
|
This cannot be done with TypedArgs because it requires the number of
|
|
inputs.
|
|
"""
|
|
inregex: T.List[str] = ['@PLAINNAME[0-9]+@', '@BASENAME[0-9]+@']
|
|
from ..utils.universal import iter_regexin_iter
|
|
for out in outputs:
|
|
match = iter_regexin_iter(inregex, [out])
|
|
if has_multi_in and ('@PLAINNAME@' in out or '@BASENAME@' in out):
|
|
raise InvalidArguments(f'{name}: output cannot contain "@PLAINNAME@" or "@BASENAME@" '
|
|
'when there is more than one input (we can\'t know which to use)')
|
|
elif match:
|
|
FeatureNew.single_use(
|
|
f'{match} in output', '1.5.0',
|
|
self.subproject)
|
|
|
|
@TypedArgs(
|
|
'custom_target',
|
|
opt_types=[STR_OARG.evolve(optional_since='0.60.0', default='')],
|
|
kw_types=[
|
|
COMMAND_KW,
|
|
CT_BUILD_ALWAYS,
|
|
CT_BUILD_ALWAYS_STALE,
|
|
CT_BUILD_BY_DEFAULT,
|
|
CT_INPUT_KW,
|
|
CT_INSTALL_DIR_KW,
|
|
CT_INSTALL_TAG_KW,
|
|
MULTI_OUTPUT_KW,
|
|
DEPENDS_KW,
|
|
DEPEND_FILES_KW,
|
|
DEPFILE_KW,
|
|
ENV_KW.evolve(since='0.57.0'),
|
|
INSTALL_KW,
|
|
INSTALL_MODE_KW.evolve(since='0.47.0'),
|
|
KwargInfo('feed', bool, default=False, since='0.59.0'),
|
|
KwargInfo('capture', bool, default=False),
|
|
KwargInfo('console', bool, default=False, since='0.48.0'),
|
|
KwargInfo('build_subdir', str, default='', since='1.10.0'),
|
|
],
|
|
)
|
|
@apply_machine_map
|
|
def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str],
|
|
kwargs: 'kwtypes.CustomTarget') -> build.CustomTarget:
|
|
if kwargs['depfile'] and ('@BASENAME@' in kwargs['depfile'] or '@PLAINNAME@' in kwargs['depfile']):
|
|
FeatureNew.single_use('substitutions in custom_target depfile', '0.47.0', self.subproject, location=node)
|
|
install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
|
|
|
|
# Don't mutate the kwargs
|
|
|
|
build_by_default = kwargs['build_by_default']
|
|
build_always_stale = kwargs['build_always_stale']
|
|
# Remap build_always to build_by_default and build_always_stale
|
|
if kwargs['build_always'] is not None and kwargs['build_always_stale'] is not None:
|
|
raise InterpreterException('CustomTarget: "build_always" and "build_always_stale" are mutually exclusive')
|
|
|
|
if build_by_default is None and kwargs['install']:
|
|
build_by_default = True
|
|
|
|
elif kwargs['build_always'] is not None:
|
|
if build_by_default is None:
|
|
build_by_default = kwargs['build_always']
|
|
build_always_stale = kwargs['build_by_default']
|
|
|
|
# These are nullable so that we can know whether they're explicitly
|
|
# set or not. If they haven't been overwritten, set them to their true
|
|
# default
|
|
if build_by_default is None:
|
|
build_by_default = False
|
|
if build_always_stale is None:
|
|
build_always_stale = False
|
|
|
|
# name will default to first output, but we cannot do that yet because
|
|
# they could need substitutions (e.g. @BASENAME@) first. CustomTarget()
|
|
# will take care of setting a proper default but name must be an empty
|
|
# string in the meantime.
|
|
name = args[0]
|
|
inputs = self.source_strings_to_files(kwargs['input'])
|
|
command = kwargs['command']
|
|
if command and isinstance(command[0], str):
|
|
command[0] = self.find_program_impl([command[0]])
|
|
|
|
if len(inputs) > 1 and kwargs['feed']:
|
|
raise InvalidArguments('custom_target: "feed" keyword argument can only be used with a single input')
|
|
if len(kwargs['output']) > 1 and kwargs['capture']:
|
|
raise InvalidArguments('custom_target: "capture" keyword argument can only be used with a single output')
|
|
if kwargs['capture'] and kwargs['console']:
|
|
raise InvalidArguments('custom_target: "capture" and "console" keyword arguments are mutually exclusive')
|
|
for c in command:
|
|
if kwargs['capture'] and isinstance(c, str) and '@OUTPUT@' in c:
|
|
raise InvalidArguments('custom_target: "capture" keyword argument cannot be used with "@OUTPUT@"')
|
|
if kwargs['feed'] and isinstance(c, str) and '@INPUT@' in c:
|
|
raise InvalidArguments('custom_target: "feed" keyword argument cannot be used with "@INPUT@"')
|
|
if kwargs['install'] and not kwargs['install_dir']:
|
|
raise InvalidArguments('custom_target: "install_dir" keyword argument must be set when "install" is true.')
|
|
if len(kwargs['install_dir']) > 1:
|
|
FeatureNew.single_use('multiple install_dir for custom_target', '0.40.0', self.subproject, location=node)
|
|
if len(kwargs['install_tag']) not in {0, 1, len(kwargs['output'])}:
|
|
raise InvalidArguments('custom_target: install_tag argument must have 0 or 1 outputs, '
|
|
'or the same number of elements as the output keyword argument. '
|
|
f'(there are {len(kwargs["install_tag"])} install_tags, '
|
|
f'and {len(kwargs["output"])} outputs)')
|
|
|
|
self._validate_custom_target_outputs(len(inputs) > 1, kwargs['output'], "custom_target")
|
|
|
|
with self.relaxing(InterpreterRuleRelaxation.ALLOW_SANDBOX_VIOLATION,
|
|
FeatureBroken('grabbing custom target depend_files from outside the current project', '1.12.0')):
|
|
depend_files = self.source_strings_to_files(kwargs['depend_files'])
|
|
|
|
tg = build.CustomTarget(
|
|
name,
|
|
self.subdir,
|
|
self.environment,
|
|
command,
|
|
inputs,
|
|
kwargs['output'],
|
|
self.current_build_project(),
|
|
build_always_stale=build_always_stale,
|
|
build_by_default=build_by_default,
|
|
capture=kwargs['capture'],
|
|
console=kwargs['console'],
|
|
depend_files=depend_files,
|
|
depfile=kwargs['depfile'],
|
|
extra_depends=kwargs['depends'],
|
|
env=kwargs['env'],
|
|
feed=kwargs['feed'],
|
|
install=kwargs['install'],
|
|
install_dir=kwargs['install_dir'],
|
|
install_mode=install_mode,
|
|
install_tag=kwargs['install_tag'],
|
|
backend=self.backend,
|
|
build_subdir=kwargs['build_subdir'])
|
|
|
|
subdir = tg.get_builddir()
|
|
for t in tg.get_outputs():
|
|
self.validate_forbidden_targets(t, not subdir)
|
|
self.add_target(tg.name, tg)
|
|
return tg
|
|
|
|
@TypedArgs(
|
|
'run_target',
|
|
pos_types=[STR_PARG],
|
|
kw_types=[
|
|
COMMAND_KW,
|
|
DEPENDS_KW,
|
|
ENV_KW.evolve(since='0.57.0'),
|
|
]
|
|
)
|
|
def func_run_target(self, node: mparser.FunctionNode, args: T.Tuple[str],
|
|
kwargs: 'kwtypes.RunTarget') -> build.RunTarget:
|
|
all_args = kwargs['command'].copy()
|
|
|
|
for i in listify(all_args):
|
|
if isinstance(i, ExternalProgram) and not i.found():
|
|
raise InterpreterException(f'Tried to use non-existing executable {i.name!r}')
|
|
if isinstance(all_args[0], str):
|
|
all_args[0] = self.find_program_impl([all_args[0]])
|
|
|
|
if '@DEPFILE@' in all_args:
|
|
raise InterpreterException('run_target does not have support for @DEPFILE@')
|
|
if '@PRIVATE_DIR@' in all_args:
|
|
raise InterpreterException('run_target does not have support for @PRIVATE_DIR@')
|
|
|
|
name = args[0]
|
|
tg = build.RunTarget(name, all_args, kwargs['depends'], self.subdir, self.environment,
|
|
self.current_build_project(), kwargs['env'])
|
|
self.add_target(name, tg)
|
|
return tg
|
|
|
|
@FeatureNew('alias_target', '0.52.0')
|
|
@TypedArgs(
|
|
'alias_target',
|
|
pos_types=[STR_PARG],
|
|
var_types=VarArgInfo(
|
|
(build.Target, build.BothLibraries),
|
|
min_args=1,
|
|
since_values={build.RunTarget: '0.60.0'},
|
|
),
|
|
)
|
|
def func_alias_target(self, node: mparser.BaseNode, args: T.Tuple[str, T.List[T.Union[build.Target, build.BothLibraries]]],
|
|
kwargs: TYPE_kwargs) -> build.AliasTarget:
|
|
name, deps = args
|
|
real_deps: T.List[build.Target] = []
|
|
for d in deps:
|
|
if isinstance(d, build.BothLibraries):
|
|
real_deps.append(d.shared)
|
|
real_deps.append(d.static)
|
|
else:
|
|
real_deps.append(d)
|
|
tg = build.AliasTarget(name, real_deps, self.subdir, self.environment,
|
|
self.current_build_project())
|
|
self.add_target(name, tg)
|
|
return tg
|
|
|
|
@TypedArgs(
|
|
'generator',
|
|
pos_types=[PosArgInfo((build.Executable, Program))],
|
|
kw_types=[
|
|
KwargInfo('arguments', ContainerTypeInfo(list, str, allow_empty=False), required=True, listify=True),
|
|
KwargInfo('output', ContainerTypeInfo(list, str, allow_empty=False), required=True, listify=True),
|
|
DEPFILE_KW,
|
|
DEPENDS_KW,
|
|
KwargInfo('capture', bool, default=False, since='0.43.0'),
|
|
]
|
|
)
|
|
def func_generator(self, node: mparser.FunctionNode,
|
|
args: T.Tuple[T.Union[build.Executable, Program]],
|
|
kwargs: 'kwtypes.FuncGenerator') -> build.Generator:
|
|
for rule in kwargs['output']:
|
|
if '@BASENAME@' not in rule and '@PLAINNAME@' not in rule:
|
|
raise InvalidArguments('Every element of "output" must contain @BASENAME@ or @PLAINNAME@.')
|
|
if has_path_sep(rule):
|
|
raise InvalidArguments('"output" must not contain a directory separator.')
|
|
if len(kwargs['output']) > 1:
|
|
for o in kwargs['output']:
|
|
if '@OUTPUT@' in o:
|
|
raise InvalidArguments('Tried to use @OUTPUT@ in a rule with more than one output.')
|
|
|
|
if isinstance(args[0], build.Executable):
|
|
exe_prg = build.LocalProgram(args[0], self.project_version)
|
|
else:
|
|
exe_prg = args[0]
|
|
return build.Generator(self.environment, exe_prg, **kwargs)
|
|
|
|
@TypedArgs(
|
|
'benchmark',
|
|
pos_types=[STR_PARG, _TEST_PARG],
|
|
kw_types=TEST_KWS,
|
|
)
|
|
def func_benchmark(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, T.Union[build.Executable, build.Jar, Program, mesonlib.File, build.CustomTarget, build.CustomTargetIndex]],
|
|
kwargs: 'kwtypes.FuncBenchmark') -> None:
|
|
self.add_test(node, args, kwargs, False)
|
|
|
|
@TypedArgs(
|
|
'test',
|
|
pos_types=[STR_PARG, _TEST_PARG],
|
|
kw_types=[*TEST_KWS, KwargInfo('is_parallel', bool, default=True)]
|
|
)
|
|
def func_test(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, T.Union[build.Executable, build.Jar, Program, mesonlib.File, build.CustomTarget, build.CustomTargetIndex]],
|
|
kwargs: 'kwtypes.FuncTest') -> None:
|
|
self.add_test(node, args, kwargs, True)
|
|
|
|
def make_test(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, T.Union[build.Executable, build.Jar, Program, mesonlib.File, build.CustomTarget, build.CustomTargetIndex]],
|
|
kwargs: kwtypes.FuncTest | kwtypes.FuncBenchmark,
|
|
klass: T.Type[TestClass] = Test) -> TestClass: # type: ignore[assignment]
|
|
name = args[0]
|
|
if ':' in name:
|
|
mlog.deprecation(f'":" is not allowed in test name "{name}", it has been replaced with "_"',
|
|
location=node)
|
|
name = name.replace(':', '_')
|
|
exe = args[1]
|
|
if isinstance(exe, Program) and not exe.found():
|
|
raise InvalidArguments('Tried to use not-found external program as test exe')
|
|
elif isinstance(exe, mesonlib.File):
|
|
exe = self.find_program_impl([exe])
|
|
if isinstance(exe, (build.Executable, build.CustomTarget, build.CustomTargetIndex)):
|
|
kwargs.setdefault('depends', []).append(exe.get_target())
|
|
|
|
expected_fail = False
|
|
if kwargs['should_fail'] is not None and kwargs['expected_fail'] is not None:
|
|
raise InvalidArguments("Tried to use both 'should_fail' and 'expected_fail'")
|
|
elif kwargs['should_fail'] is not None:
|
|
expected_fail = kwargs['should_fail']
|
|
elif kwargs['expected_fail'] is not None:
|
|
expected_fail = kwargs['expected_fail']
|
|
|
|
prj = self.subproject if self.is_subproject() else self.build.project_name
|
|
|
|
suite: T.List[str] = []
|
|
for s in kwargs['suite']:
|
|
if s:
|
|
s = ':' + s
|
|
suite.append(prj.replace(' ', '_').replace(':', '_') + s)
|
|
|
|
return klass(name,
|
|
prj,
|
|
suite,
|
|
exe,
|
|
kwargs['depends'],
|
|
T.cast('bool', kwargs.get('is_parallel', False)),
|
|
kwargs['args'],
|
|
kwargs['env'],
|
|
expected_fail,
|
|
kwargs['expected_exitcode'],
|
|
kwargs['timeout'],
|
|
kwargs['workdir'],
|
|
kwargs['protocol'],
|
|
kwargs['priority'],
|
|
kwargs['verbose'])
|
|
|
|
def add_test(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str, T.Union[build.Executable, build.Jar, Program, mesonlib.File, build.CustomTarget, build.CustomTargetIndex]],
|
|
kwargs: kwtypes.FuncTest | kwtypes.FuncBenchmark, is_base_test: bool) -> None:
|
|
if isinstance(args[1], (build.CustomTarget, build.CustomTargetIndex)):
|
|
FeatureNew.single_use('test with CustomTarget as command', '1.4.0', self.subproject)
|
|
|
|
t: Test = self.make_test(node, args, kwargs)
|
|
if is_base_test:
|
|
self.build.tests.append(t)
|
|
mlog.debug('Adding test', mlog.bold(t.name, True))
|
|
else:
|
|
self.build.benchmarks.append(t)
|
|
mlog.debug('Adding benchmark', mlog.bold(t.name, True))
|
|
|
|
@TypedArgs(
|
|
'install_headers',
|
|
var_types=STR_FILE_VARG,
|
|
kw_types=[
|
|
PRESERVE_PATH_KW,
|
|
KwargInfo('subdir', (str, NoneType)),
|
|
INSTALL_MODE_KW.evolve(since='0.47.0'),
|
|
INSTALL_DIR_KW,
|
|
INSTALL_FOLLOW_SYMLINKS,
|
|
INSTALL_TAG_KW.evolve(since='1.11.0'),
|
|
],
|
|
)
|
|
def func_install_headers(self, node: mparser.BaseNode,
|
|
args: T.Tuple[T.List['mesonlib.FileOrString']],
|
|
kwargs: 'kwtypes.FuncInstallHeaders') -> list[build.Headers]:
|
|
install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
|
|
source_files = self.source_strings_to_files(args[0])
|
|
install_subdir = kwargs['subdir']
|
|
if install_subdir is not None:
|
|
if kwargs['install_dir'] is not None:
|
|
raise InterpreterException('install_headers: cannot specify both "install_dir" and "subdir". Use only "install_dir".')
|
|
if path_has_root(install_subdir):
|
|
mlog.deprecation('Subdir keyword must not be an absolute path. This will be a hard error in meson 2.0.')
|
|
else:
|
|
install_subdir = ''
|
|
|
|
dirs = collections.defaultdict(list)
|
|
ret_headers: list[build.Headers] = []
|
|
if kwargs['preserve_path']:
|
|
for file in source_files:
|
|
dirname = os.path.dirname(file.fname)
|
|
dirs[dirname].append(file)
|
|
else:
|
|
dirs[''].extend(source_files)
|
|
|
|
for childdir in dirs:
|
|
h = build.Headers(dirs[childdir], os.path.join(install_subdir, childdir), kwargs['install_dir'],
|
|
install_mode, self.subproject,
|
|
follow_symlinks=kwargs['follow_symlinks'],
|
|
install_tag=kwargs['install_tag'])
|
|
ret_headers.append(h)
|
|
if not self.is_internal_machine(MachineChoice.HOST):
|
|
self.build.headers.append(h)
|
|
|
|
return ret_headers
|
|
|
|
@TypedArgs(
|
|
'install_man',
|
|
var_types=STR_FILE_VARG,
|
|
kw_types=[
|
|
KwargInfo('locale', (str, NoneType), since='0.58.0'),
|
|
INSTALL_MODE_KW.evolve(since='0.47.0'),
|
|
INSTALL_DIR_KW,
|
|
INSTALL_TAG_KW.evolve(since='1.11.0')
|
|
],
|
|
)
|
|
def func_install_man(self, node: mparser.BaseNode,
|
|
args: T.Tuple[T.List['mesonlib.FileOrString']],
|
|
kwargs: 'kwtypes.FuncInstallMan') -> build.Man:
|
|
install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
|
|
# We just need to narrow this, because the input is limited to files and
|
|
# Strings as inputs, so only Files will be returned
|
|
sources = self.source_strings_to_files(args[0])
|
|
for s in sources:
|
|
try:
|
|
num = int(s.rsplit('.', 1)[-1])
|
|
except (IndexError, ValueError):
|
|
num = 0
|
|
if not 1 <= num <= 9:
|
|
raise InvalidArguments('Man file must have a file extension of a number between 1 and 9')
|
|
|
|
m = build.Man(sources, kwargs['install_dir'], install_mode,
|
|
self.subproject, kwargs['locale'], kwargs['install_tag'])
|
|
if not self.is_internal_machine(MachineChoice.HOST):
|
|
self.build.man.append(m)
|
|
|
|
return m
|
|
|
|
@FeatureNew('install_emptydir', '0.60.0')
|
|
@TypedArgs(
|
|
'install_emptydir',
|
|
pos_types=[STR_PARG],
|
|
kw_types=[
|
|
INSTALL_MODE_KW,
|
|
KwargInfo('install_tag', (str, NoneType), since='0.62.0')
|
|
],
|
|
)
|
|
def func_install_emptydir(self, node: mparser.BaseNode, args: T.Tuple[str],
|
|
kwargs: kwtypes.FuncInstallEmptyDir) -> build.EmptyDir:
|
|
d = build.EmptyDir(args[0], kwargs['install_mode'], self.subproject, kwargs['install_tag'])
|
|
if not self.is_internal_machine(MachineChoice.HOST):
|
|
self.build.emptydir.append(d)
|
|
return d
|
|
|
|
@FeatureNew('install_symlink', '0.61.0')
|
|
@TypedArgs(
|
|
'install_symlink',
|
|
pos_types=[STR_PARG],
|
|
kw_types=[
|
|
KwargInfo('pointing_to', str, required=True),
|
|
KwargInfo('install_dir', str, required=True),
|
|
INSTALL_TAG_KW,
|
|
],
|
|
)
|
|
def func_install_symlink(self, node: mparser.BaseNode,
|
|
args: T.Tuple[str],
|
|
kwargs: kwtypes.FuncInstallSymlink) -> build.SymlinkData:
|
|
name = args[0] # Validation while creating the SymlinkData object
|
|
target = kwargs['pointing_to']
|
|
l = build.SymlinkData(target, name, kwargs['install_dir'],
|
|
self.subproject, kwargs['install_tag'])
|
|
if not self.is_internal_machine(MachineChoice.HOST):
|
|
self.build.symlinks.append(l)
|
|
return l
|
|
|
|
@FeatureNew('structured_sources', '0.62.0')
|
|
@TypedArgs(
|
|
'structured_sources',
|
|
pos_types=[TGT_PARG],
|
|
opt_types=[OptArgInfo(dict)], # TODO: ContainerType cannot represent dict[str, list[...Ts]]
|
|
)
|
|
@noArgsFlattening
|
|
def func_structured_sources(
|
|
self, node: mparser.BaseNode,
|
|
args: T.Tuple[list[str | mesonlib.File | build.GeneratedTypes], T.Optional[T.Dict[str, object]]],
|
|
kwargs: 'TYPE_kwargs') -> build.StructuredSources:
|
|
sources: T.DefaultDict[str, T.List[T.Union[build.TargetSources]]] = collections.defaultdict(list)
|
|
|
|
def add(key: str, value: object) -> None:
|
|
items: T.List[T.Union[str, build.TargetSources]] = []
|
|
for arg in mesonlib.listify(value):
|
|
if not isinstance(arg, (str, mesonlib.File, build.GeneratedList,
|
|
build.CustomTarget, build.CustomTargetIndex)):
|
|
raise InvalidArguments(f'structured_sources: type "{type(arg)}" is not valid')
|
|
items.append(arg)
|
|
with self.relaxing(InterpreterRuleRelaxation.ALLOW_SANDBOX_VIOLATION,
|
|
FeatureBroken('grabbing structured_sources from outside the current project', '1.13.0')):
|
|
sources[key].extend(self.source_strings_to_files(items))
|
|
|
|
add('', args[0])
|
|
if args[1]:
|
|
if '' in args[1]:
|
|
raise InvalidArguments('structured_sources: keys to dictionary argument may not be an empty string.')
|
|
for k, v in args[1].items():
|
|
add(k, v)
|
|
|
|
result = build.StructuredSources(sources)
|
|
if result.needs_copy and not result.has_built_files():
|
|
FeatureNew.single_use('structured_sources() that only remaps source files', '1.13.0', self.subproject,
|
|
'This was broken and would not copy the source files to the build tree', node)
|
|
return result
|
|
|
|
@TypedArgs(
|
|
'subdir',
|
|
pos_types=[STR_PARG],
|
|
kw_types=[
|
|
KwargInfo(
|
|
'if_found',
|
|
ContainerTypeInfo(list, object),
|
|
validator=lambda a: 'Objects must have a found() method' if not all(hasattr(x, 'found') for x in a) else None,
|
|
since='0.44.0',
|
|
default=[],
|
|
listify=True,
|
|
),
|
|
],
|
|
)
|
|
def func_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'kwtypes.Subdir') -> None:
|
|
mesonlib.check_direntry_issues(args)
|
|
if '..' in args[0]:
|
|
raise InvalidArguments('Subdir contains ..')
|
|
if self.subdir == '' and args[0] == self.subproject_dir:
|
|
raise InvalidArguments('Must not go into subprojects dir with subdir(), use subproject() instead.')
|
|
if self.subdir == '' and args[0].startswith('meson-'):
|
|
raise InvalidArguments('The "meson-" prefix is reserved and cannot be used for top-level subdir().')
|
|
if args[0] == '':
|
|
raise InvalidArguments("The argument given to subdir() is the empty string ''. This is prohibited.")
|
|
if path_has_root(args[0]):
|
|
raise InvalidArguments('Subdir argument must be a relative path.')
|
|
for i in kwargs['if_found']:
|
|
if not i.found():
|
|
return
|
|
|
|
subdir, is_new = self._resolve_subdir(self.environment.get_source_dir(), args[0])
|
|
if not is_new:
|
|
raise InvalidArguments(f'Tried to enter directory "{subdir}", which has already been visited.')
|
|
|
|
self.create_build_subdir(subdir)
|
|
|
|
if self.relaxed(InterpreterRuleRelaxation.CARGO_SUBDIR) and \
|
|
os.path.exists(os.path.join(self.environment.get_source_dir(), subdir, 'Cargo.toml')):
|
|
codeblock = self.cargo.interpret(subdir, self.root_subdir)
|
|
self._save_ast(subdir, codeblock)
|
|
self._evaluate_codeblock(codeblock, subdir)
|
|
elif not self._evaluate_subdir(self.environment.get_source_dir(), subdir):
|
|
buildfilename = os.path.join(subdir, environment.build_filename)
|
|
raise InterpreterException(f"Nonexistent build file '{buildfilename!s}'")
|
|
|
|
# This is either ignored on basically any OS nowadays, or silently gets
|
|
# ignored (Solaris) or triggers an "illegal operation" error (FreeBSD).
|
|
# It was likely added "because it exists", but should never be used. In
|
|
# theory it is useful for directories, but we never apply modes to
|
|
# directories other than in install_emptydir.
|
|
def _warn_kwarg_install_mode_sticky(self, mode: FileMode) -> FileMode:
|
|
if mode.perms > 0 and mode.perms & stat.S_ISVTX:
|
|
mlog.deprecation('install_mode with the sticky bit on a file does not do anything and will '
|
|
'be ignored since Meson 0.64.0', location=self.current_node)
|
|
perms = stat.filemode(mode.perms - stat.S_ISVTX)[1:]
|
|
return FileMode(perms, mode.owner, mode.group)
|
|
else:
|
|
return mode
|
|
|
|
@TypedArgs(
|
|
'install_data',
|
|
var_types=STR_FILE_VARG,
|
|
kw_types=[
|
|
KwargInfo('sources', ContainerTypeInfo(list, (str, mesonlib.File)), listify=True, default=[]),
|
|
KwargInfo('rename', ContainerTypeInfo(list, str), default=[], listify=True, since='0.46.0'),
|
|
INSTALL_MODE_KW.evolve(since='0.38.0'),
|
|
INSTALL_TAG_KW.evolve(since='0.60.0'),
|
|
INSTALL_DIR_KW,
|
|
PRESERVE_PATH_KW.evolve(since='0.64.0'),
|
|
INSTALL_FOLLOW_SYMLINKS,
|
|
],
|
|
)
|
|
def func_install_data(self, node: mparser.BaseNode,
|
|
args: T.Tuple[T.List['mesonlib.FileOrString']],
|
|
kwargs: 'kwtypes.FuncInstallData') -> list[build.Data]:
|
|
sources = self.source_strings_to_files(args[0] + kwargs['sources'])
|
|
rename = kwargs['rename'] or None
|
|
if rename:
|
|
if len(rename) != len(sources):
|
|
raise InvalidArguments(
|
|
'"rename" and "sources" argument lists must be the same length if "rename" is given. '
|
|
f'Rename has {len(rename)} elements and sources has {len(sources)}.')
|
|
|
|
install_dir = kwargs['install_dir']
|
|
if not install_dir:
|
|
subdir = self.active_projectname
|
|
install_dir = P_OBJ.OptionString(os.path.join(self.environment.get_datadir(), subdir), os.path.join('{datadir}', subdir))
|
|
if self.is_subproject():
|
|
FeatureNew.single_use('install_data() without install_dir inside of a subproject', '1.3.0', self.subproject,
|
|
'This was broken and would install to the project name of the parent project instead',
|
|
node)
|
|
if kwargs['preserve_path']:
|
|
FeatureNew.single_use('install_data() with preserve_path and without install_dir', '1.3.0', self.subproject,
|
|
'This was broken and would not add the project name to the install path',
|
|
node)
|
|
|
|
install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
|
|
return self.install_data_impl(sources, install_dir, install_mode, rename, kwargs['install_tag'],
|
|
preserve_path=kwargs['preserve_path'],
|
|
follow_symlinks=kwargs['follow_symlinks'])
|
|
|
|
def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: str,
|
|
install_mode: FileMode, rename: T.Optional[T.List[str]],
|
|
tag: T.Optional[str],
|
|
install_data_type: T.Optional[str] = None,
|
|
preserve_path: bool = False,
|
|
follow_symlinks: T.Optional[bool] = None) -> list[build.Data]:
|
|
install_dir_name = install_dir.optname if isinstance(install_dir, P_OBJ.OptionString) else install_dir
|
|
dirs = collections.defaultdict(list)
|
|
if preserve_path:
|
|
for file in sources:
|
|
dirname = os.path.dirname(file.fname)
|
|
dirs[dirname].append(file)
|
|
else:
|
|
dirs[''].extend(sources)
|
|
|
|
ret_data: list[build.Data] = []
|
|
for childdir, files in dirs.items():
|
|
d = build.Data(files, os.path.join(install_dir, childdir), os.path.join(install_dir_name, childdir),
|
|
install_mode, self.subproject, rename, tag, install_data_type,
|
|
follow_symlinks)
|
|
ret_data.append(d)
|
|
|
|
if not self.is_internal_machine(MachineChoice.HOST):
|
|
self.build.data.extend(ret_data)
|
|
return ret_data
|
|
|
|
@TypedArgs(
|
|
'install_subdir',
|
|
pos_types=[STR_PARG],
|
|
kw_types=[
|
|
KwargInfo('install_dir', str, required=True),
|
|
KwargInfo('strip_directory', bool, default=False),
|
|
KwargInfo(
|
|
'exclude_files', ContainerTypeInfo(list, str),
|
|
default=[], listify=True, since='0.42.0',
|
|
validator=lambda x: 'cannot be absolute' if any(path_has_root(d) for d in x) else None,
|
|
),
|
|
KwargInfo(
|
|
'exclude_directories', ContainerTypeInfo(list, str),
|
|
default=[], listify=True, since='0.42.0',
|
|
validator=lambda x: 'cannot be absolute' if any(path_has_root(d) for d in x) else None,
|
|
),
|
|
INSTALL_MODE_KW.evolve(since='0.38.0'),
|
|
INSTALL_TAG_KW.evolve(since='0.60.0'),
|
|
INSTALL_FOLLOW_SYMLINKS,
|
|
],
|
|
)
|
|
def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str],
|
|
kwargs: 'kwtypes.FuncInstallSubdir') -> build.InstallDir:
|
|
exclude = (set(kwargs['exclude_files']), set(kwargs['exclude_directories']))
|
|
|
|
srcdir = os.path.join(self.environment.source_dir, self.subdir, args[0])
|
|
if not os.path.isdir(srcdir) or not any(os.listdir(srcdir)):
|
|
FeatureNew.single_use('install_subdir with empty directory', '0.47.0', self.subproject, location=node)
|
|
FeatureDeprecated.single_use('install_subdir with empty directory', '0.60.0', self.subproject,
|
|
'It worked by accident and is buggy. Use install_emptydir instead.', node)
|
|
install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
|
|
|
|
idir_name = kwargs['install_dir']
|
|
if isinstance(idir_name, P_OBJ.OptionString):
|
|
idir_name = idir_name.optname
|
|
|
|
idir = build.InstallDir(
|
|
self.subdir,
|
|
args[0],
|
|
kwargs['install_dir'],
|
|
idir_name,
|
|
install_mode,
|
|
exclude,
|
|
kwargs['strip_directory'],
|
|
self.subproject,
|
|
install_tag=kwargs['install_tag'],
|
|
follow_symlinks=kwargs['follow_symlinks'])
|
|
if not self.is_internal_machine(MachineChoice.HOST):
|
|
self.build.install_dirs.append(idir)
|
|
return idir
|
|
|
|
def validate_build_subdir(self, build_subdir: str, target: str) -> None:
|
|
if build_subdir and build_subdir != '.':
|
|
if os.path.exists(os.path.join(self.source_root, self.subdir, build_subdir)):
|
|
raise InvalidArguments(f'Build subdir "{build_subdir}" in "{target}" exists in source tree.')
|
|
if '..' in build_subdir:
|
|
raise InvalidArguments(f'Build subdir "{build_subdir}" in "{target}" contains ..')
|
|
self.create_build_subdir(os.path.join(self.current_build_dir(), build_subdir))
|
|
|
|
@TypedArgs(
|
|
'configure_file',
|
|
kw_types=[
|
|
DEPFILE_KW.evolve(since='0.52.0'),
|
|
INSTALL_MODE_KW.evolve(since='0.47.0,'),
|
|
INSTALL_TAG_KW.evolve(since='0.60.0'),
|
|
KwargInfo('capture', bool, default=False, since='0.41.0'),
|
|
KwargInfo(
|
|
'command',
|
|
(ContainerTypeInfo(list, (build.Executable, Program, compilers.Compiler, mesonlib.File, str), allow_empty=False), NoneType),
|
|
listify=True,
|
|
),
|
|
KwargInfo(
|
|
'configuration',
|
|
(ContainerTypeInfo(dict, (str, int, bool)), build.ConfigurationData, NoneType),
|
|
),
|
|
KwargInfo(
|
|
'copy', bool, default=False, since='0.47.0',
|
|
),
|
|
KwargInfo('encoding', str, default='utf-8', since='0.47.0'),
|
|
KwargInfo(
|
|
'format',
|
|
str,
|
|
default='meson',
|
|
since='0.46.0',
|
|
validator=in_set_validator({'meson', 'cmake', 'cmake@'})),
|
|
KwargInfo(
|
|
'input',
|
|
ContainerTypeInfo(list, (mesonlib.File, str)),
|
|
listify=True,
|
|
default=[],
|
|
),
|
|
# Cannot use shared implementation until None backwards compat is dropped
|
|
KwargInfo('install', (bool, NoneType), since='0.50.0'),
|
|
KwargInfo(
|
|
'install_dir',
|
|
(str, bool),
|
|
default='',
|
|
validator=lambda x: 'must be `false` if boolean' if x is True else None
|
|
),
|
|
OUTPUT_KW,
|
|
KwargInfo(
|
|
'output_format',
|
|
str,
|
|
default='c',
|
|
since='0.47.0',
|
|
since_values={'json': '1.3.0'},
|
|
validator=in_set_validator({'c', 'json', 'nasm'}),
|
|
),
|
|
KwargInfo('macro_name', (str, NoneType), default=None, since='1.3.0'),
|
|
KwargInfo('build_subdir', str, default='', since='1.10.0'),
|
|
],
|
|
)
|
|
@apply_machine_map
|
|
def func_configure_file(self, node: mparser.BaseNode, args: T.List[TYPE_var],
|
|
kwargs: kwtypes.ConfigureFile) -> mesonlib.File:
|
|
actions = sorted(x for x in ('configuration', 'command', 'copy')
|
|
if kwargs[x] not in [None, False]) # type: ignore[literal-required]
|
|
num_actions = len(actions)
|
|
if num_actions == 0:
|
|
raise InterpreterException('Must specify an action with one of these '
|
|
'keyword arguments: \'configuration\', '
|
|
'\'command\', or \'copy\'.')
|
|
elif num_actions == 2:
|
|
raise InterpreterException('Must not specify both {!r} and {!r} '
|
|
'keyword arguments since they are '
|
|
'mutually exclusive.'.format(*actions))
|
|
elif num_actions == 3:
|
|
raise InterpreterException('Must specify one of {!r}, {!r}, and '
|
|
'{!r} keyword arguments since they are '
|
|
'mutually exclusive.'.format(*actions))
|
|
|
|
if kwargs['capture'] and not kwargs['command']:
|
|
raise InvalidArguments('configure_file: "capture" keyword requires "command" keyword.')
|
|
|
|
install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
|
|
|
|
fmt = kwargs['format']
|
|
output_format = kwargs['output_format']
|
|
depfile = kwargs['depfile']
|
|
|
|
# Validate input
|
|
inputs = self.source_strings_to_files(kwargs['input'])
|
|
inputs_abs = []
|
|
for f in inputs:
|
|
if isinstance(f, mesonlib.File):
|
|
inputs_abs.append(f.absolute_path(self.environment.source_dir,
|
|
self.environment.build_dir))
|
|
self.add_build_def_file(f)
|
|
else:
|
|
raise InterpreterException('Inputs can only be strings or file objects')
|
|
|
|
# Validate output
|
|
output = kwargs['output']
|
|
if inputs_abs:
|
|
values = mesonlib.get_filenames_templates_dict(inputs_abs, None)
|
|
outputs = mesonlib.substitute_values([output], values)
|
|
output = outputs[0]
|
|
if depfile:
|
|
depfile = mesonlib.substitute_values([depfile], values)[0]
|
|
|
|
# Validate build_subdir
|
|
build_subdir = kwargs['build_subdir']
|
|
self.validate_build_subdir(build_subdir, output)
|
|
|
|
ofile_rpath = os.path.join(self.current_build_dir(), build_subdir, output)
|
|
if ofile_rpath in self.configure_file_outputs:
|
|
mesonbuildfile = os.path.join(self.subdir, 'meson.build')
|
|
current_call = f"{mesonbuildfile}:{self.current_node.lineno}"
|
|
first_call = "{}:{}".format(mesonbuildfile, self.configure_file_outputs[ofile_rpath])
|
|
mlog.warning('Output file', mlog.bold(ofile_rpath, True), 'for configure_file() at', current_call, 'overwrites configure_file() output at', first_call)
|
|
else:
|
|
self.configure_file_outputs[ofile_rpath] = self.current_node.lineno
|
|
|
|
ofile_path, ofile_fname = os.path.split(ofile_rpath)
|
|
ofile_abs = os.path.join(self.environment.build_dir, ofile_rpath)
|
|
os.makedirs(os.path.split(ofile_abs)[0], exist_ok=True)
|
|
|
|
# Perform the appropriate action
|
|
if kwargs['configuration'] is not None:
|
|
conf = kwargs['configuration']
|
|
if isinstance(conf, dict):
|
|
FeatureNew.single_use('configure_file.configuration dictionary', '0.49.0', self.subproject, location=node)
|
|
for k, v in conf.items():
|
|
if not isinstance(v, (str, int, bool)):
|
|
raise InvalidArguments(
|
|
f'"configuration_data": initial value dictionary key "{k!r}"" must be "str | int | bool", not "{v!r}"')
|
|
conf = build.ConfigurationData(conf)
|
|
mlog.log('Configuring', mlog.bold(os.path.join(build_subdir, output)), 'using configuration')
|
|
if len(inputs) > 1:
|
|
raise InterpreterException('At most one input file can given in configuration mode')
|
|
if inputs:
|
|
file_encoding = kwargs['encoding']
|
|
missing_variables, confdata_useless = \
|
|
mesonlib.do_conf_file(inputs_abs[0], ofile_abs, conf,
|
|
fmt, file_encoding, self.subproject)
|
|
if missing_variables:
|
|
var_list = ", ".join(repr(m) for m in sorted(missing_variables))
|
|
mlog.warning(
|
|
f"The variable(s) {var_list} in the input file '{inputs[0]}' are not "
|
|
"present in the given configuration data.", location=node)
|
|
if confdata_useless:
|
|
ifbase = os.path.basename(inputs_abs[0])
|
|
tv = FeatureNew.get_target_version(self.subproject)
|
|
if FeatureNew.check_version(tv, '0.47'):
|
|
mlog.warning('Got an empty configuration_data() object and found no '
|
|
f'substitutions in the input file {ifbase!r}. If you want to '
|
|
'copy a file to the build dir, use the \'copy:\' keyword '
|
|
'argument added in 0.47.0', location=node)
|
|
else:
|
|
macro_name = kwargs['macro_name']
|
|
mesonlib.dump_conf_header(ofile_abs, conf, output_format, macro_name)
|
|
conf.used = True
|
|
elif kwargs['command'] is not None:
|
|
if len(inputs) > 1:
|
|
FeatureNew.single_use('multiple inputs in configure_file()', '0.52.0', self.subproject, location=node)
|
|
# We use absolute paths for input and output here because the cwd
|
|
# that the command is run from is 'unspecified', so it could change.
|
|
# Currently it's builddir/subdir for in_builddir else srcdir/subdir.
|
|
values = mesonlib.get_filenames_templates_dict(inputs_abs, [ofile_abs])
|
|
if depfile:
|
|
depfile = os.path.join(self.environment.get_scratch_dir(), depfile)
|
|
values['@DEPFILE@'] = depfile
|
|
# Substitute @INPUT@, @OUTPUT@, etc here.
|
|
_cmd = mesonlib.substitute_values(kwargs['command'], values)
|
|
mlog.log('Configuring', mlog.bold(output), 'with command')
|
|
cmd, *_args = _cmd
|
|
res = self.run_command_impl((cmd, _args),
|
|
{'capture': True,
|
|
'console': False,
|
|
'check': True,
|
|
'env': mesonlib.EnvironmentVariables()},
|
|
True)
|
|
if kwargs['capture']:
|
|
dst_tmp = ofile_abs + '~'
|
|
file_encoding = kwargs['encoding']
|
|
with open(dst_tmp, 'w', encoding=file_encoding) as f:
|
|
f.writelines(res.stdout)
|
|
if inputs_abs:
|
|
shutil.copymode(inputs_abs[0], dst_tmp)
|
|
mesonlib.replace_if_different(ofile_abs, dst_tmp)
|
|
if depfile:
|
|
mlog.log('Reading depfile:', mlog.bold(depfile))
|
|
with open(depfile, encoding='utf-8') as f:
|
|
df = DepFile(f.readlines(), self.environment.build_dir)
|
|
deps = df.get_all_dependencies(ofile_fname)
|
|
for dep in deps:
|
|
self.add_build_def_file(dep)
|
|
|
|
elif kwargs['copy']:
|
|
if len(inputs_abs) != 1:
|
|
raise InterpreterException('Exactly one input file must be given in copy mode')
|
|
self.create_build_subdir(self.current_build_dir())
|
|
shutil.copyfile(inputs_abs[0], ofile_abs)
|
|
src_stat = os.stat(inputs_abs[0])
|
|
os.utime(ofile_abs, ns=(src_stat.st_atime_ns, src_stat.st_mtime_ns))
|
|
|
|
# Install file if requested, we check for the empty string
|
|
# for backwards compatibility. That was the behaviour before
|
|
# 0.45.0 so preserve it.
|
|
idir = kwargs['install_dir']
|
|
if idir is False:
|
|
idir = ''
|
|
FeatureDeprecated.single_use('configure_file install_dir: false', '0.50.0',
|
|
self.subproject, 'Use the `install:` kwarg instead', location=node)
|
|
install = kwargs['install'] if kwargs['install'] is not None else idir != ''
|
|
if install:
|
|
if not idir:
|
|
raise InterpreterException(
|
|
'"install_dir" must be specified when "install" in a configure_file is true')
|
|
idir_name = idir
|
|
if isinstance(idir_name, P_OBJ.OptionString):
|
|
idir_name = idir_name.optname
|
|
cfile = mesonlib.File.from_built_file(ofile_path, ofile_fname)
|
|
install_tag = kwargs['install_tag']
|
|
self.build.data.append(build.Data([cfile], idir, idir_name, install_mode, self.subproject,
|
|
install_tag=install_tag, data_type='configure'))
|
|
return mesonlib.File.from_built_file(self.current_build_dir(), output)
|
|
|
|
def extract_incdirs(self, prospectives: T.List[T.Union[str, build.IncludeDirs]],
|
|
is_d_import_dirs: bool = False
|
|
) -> T.List[build.IncludeDirs]:
|
|
result: T.List[build.IncludeDirs] = []
|
|
for p in prospectives:
|
|
if isinstance(p, build.IncludeDirs):
|
|
result.append(p)
|
|
else:
|
|
if is_d_import_dirs and os.path.normpath(p).startswith(self.environment.get_source_dir()):
|
|
FeatureDeprecated.single_use('Building absolute path to source dir is not supported',
|
|
'0.45', self.subproject,
|
|
'Use a relative path instead.',
|
|
location=self.current_node)
|
|
p = os.path.relpath(p, os.path.join(self.environment.get_source_dir(), self.subdir))
|
|
result.append(self.build_incdir_object([p]))
|
|
return result
|
|
|
|
@TypedArgs(
|
|
'include_directories',
|
|
var_types=STR_VARG,
|
|
kw_types=[KwargInfo('is_system', bool, default=False)]
|
|
)
|
|
def func_include_directories(self, node: mparser.BaseNode, args: T.Tuple[T.List[str]],
|
|
kwargs: 'kwtypes.FuncIncludeDirectories') -> build.IncludeDirs:
|
|
return self.build_incdir_object(args[0], kwargs['is_system'])
|
|
|
|
def build_incdir_object(self, incdir_strings: T.List[str], is_system: bool = False) -> build.IncludeDirs:
|
|
if not isinstance(is_system, bool):
|
|
raise InvalidArguments('Is_system must be boolean.')
|
|
src_root = self.environment.get_source_dir()
|
|
build_root = self.environment.get_build_dir()
|
|
absbase_src = os.path.join(src_root, self.subdir)
|
|
absbase_build = os.path.join(build_root, self.subdir)
|
|
|
|
for a in incdir_strings:
|
|
if path_is_in_root(Path(a), Path(src_root)):
|
|
raise InvalidArguments(textwrap.dedent('''\
|
|
Tried to form an absolute path to a dir in the source tree.
|
|
You should not do that but use relative paths instead, for
|
|
directories that are part of your project.
|
|
|
|
To get include path to any directory relative to the current dir do
|
|
|
|
incdir = include_directories(dirname)
|
|
|
|
After this incdir will contain both the current source dir as well as the
|
|
corresponding build dir. It can then be used in any subdirectory and
|
|
Meson will take care of all the busywork to make paths work.
|
|
|
|
Dirname can even be '.' to mark the current directory. Though you should
|
|
remember that the current source and build directories are always
|
|
put in the include directories by default so you only need to do
|
|
include_directories('.') if you intend to use the result in a
|
|
different subdirectory.
|
|
|
|
Note that this error message can also be triggered by
|
|
external dependencies being installed within your source
|
|
tree - it's not recommended to do this.
|
|
'''))
|
|
else:
|
|
try:
|
|
self.validate_within_subproject(self.subdir, a)
|
|
except InterpreterException:
|
|
mlog.warning('include_directories sandbox violation!', location=self.current_node)
|
|
print(textwrap.dedent(f'''\
|
|
The project is trying to access the directory {a!r} which belongs to a different
|
|
subproject. This is a problem as it hardcodes the relative paths of these two projects.
|
|
This makes it impossible to compile the project in any other directory layout and also
|
|
prevents the subproject from changing its own directory layout.
|
|
|
|
Instead of poking directly at the internals the subproject should be executed and
|
|
it should set a variable that the caller can then use. Something like:
|
|
|
|
# In subproject
|
|
some_dep = declare_dependency(include_directories: include_directories('include'))
|
|
|
|
# In subproject wrap file
|
|
[provide]
|
|
some = some_dep
|
|
|
|
# In parent project
|
|
some_dep = dependency('some')
|
|
executable(..., dependencies: [some_dep])
|
|
|
|
This warning will become a hard error in a future Meson release.
|
|
'''))
|
|
absdir_src = os.path.join(absbase_src, a)
|
|
absdir_build = os.path.join(absbase_build, a)
|
|
if not os.path.isdir(absdir_src) and not os.path.isdir(absdir_build):
|
|
raise InvalidArguments(f'Include dir {a} does not exist.')
|
|
i = build.IncludeDirs(self.subdir, incdir_strings, is_system, self.current_build_project())
|
|
return i
|
|
|
|
@TypedArgs(
|
|
'add_test_setup',
|
|
pos_types=[STR_PARG],
|
|
kw_types=[
|
|
KwargInfo('exe_wrapper', ContainerTypeInfo(list, (str, ExternalProgram)), listify=True, default=[]),
|
|
KwargInfo('gdb', bool, default=False),
|
|
KwargInfo('timeout_multiplier', int, default=1),
|
|
KwargInfo('exclude_suites', ContainerTypeInfo(list, str), listify=True, default=[], since='0.57.0'),
|
|
KwargInfo('is_default', bool, default=False, since='0.49.0'),
|
|
ENV_KW,
|
|
],
|
|
)
|
|
def func_add_test_setup(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'kwtypes.AddTestSetup') -> None:
|
|
setup_name = args[0]
|
|
if re.fullmatch('([_a-zA-Z][_0-9a-zA-Z]*:)?[_a-zA-Z][_0-9a-zA-Z]*', setup_name) is None:
|
|
raise InterpreterException('Setup name may only contain alphanumeric characters.')
|
|
if ":" not in setup_name:
|
|
setup_name = f'{(self.subproject if self.subproject else self.build.project_name)}:{setup_name}'
|
|
|
|
exe_wrapper: T.List[str] = []
|
|
for i in kwargs['exe_wrapper']:
|
|
if isinstance(i, str):
|
|
exe_wrapper.append(i)
|
|
else:
|
|
if not i.found():
|
|
raise InterpreterException('Tried to use non-found executable.')
|
|
exe_wrapper += i.get_command()
|
|
|
|
timeout_multiplier = kwargs['timeout_multiplier']
|
|
if timeout_multiplier <= 0:
|
|
FeatureNew('add_test_setup() timeout_multiplier <= 0', '0.57.0').use(self.subproject)
|
|
|
|
if kwargs['is_default']:
|
|
if self.build.test_setup_default_name is not None:
|
|
raise InterpreterException(f'{self.build.test_setup_default_name!r} is already set as default. '
|
|
'is_default can be set to true only once')
|
|
self.build.test_setup_default_name = setup_name
|
|
self.build.test_setups[setup_name] = build.TestSetup(exe_wrapper, kwargs['gdb'], timeout_multiplier, kwargs['env'],
|
|
kwargs['exclude_suites'])
|
|
|
|
@TypedArgs(
|
|
'add_global_arguments',
|
|
var_types=STR_VARG,
|
|
kw_types=[NATIVE_KW, LANGUAGE_KW]
|
|
)
|
|
def func_add_global_arguments(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
|
|
self._add_global_arguments(node, self.build.global_args[kwargs['native']], args[0], kwargs)
|
|
|
|
@TypedArgs(
|
|
'add_global_link_arguments',
|
|
var_types=STR_VARG,
|
|
kw_types=[NATIVE_KW, LANGUAGE_KW],
|
|
)
|
|
def func_add_global_link_arguments(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
|
|
self._add_global_arguments(node, self.build.global_link_args[kwargs['native']], args[0], kwargs)
|
|
|
|
@TypedArgs(
|
|
'add_project_arguments',
|
|
var_types=STR_VARG,
|
|
kw_types=[NATIVE_KW, LANGUAGE_KW],
|
|
)
|
|
def func_add_project_arguments(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
|
|
self._add_project_arguments(node, self.current_build_project().project_args[kwargs['native']],
|
|
args[0], kwargs)
|
|
|
|
@TypedArgs(
|
|
'add_project_link_arguments',
|
|
var_types=STR_VARG,
|
|
kw_types=[NATIVE_KW, LANGUAGE_KW],
|
|
)
|
|
def func_add_project_link_arguments(self, node: mparser.FunctionNode, args: T.Tuple[T.List[str]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
|
|
self._add_project_arguments(node, self.current_build_project().project_link_args[kwargs['native']],
|
|
args[0], kwargs)
|
|
|
|
def current_build_project(self) -> build.BuildProject:
|
|
for_machine = self.build.machine_map.host
|
|
return self.build.projects[for_machine][self.subproject]
|
|
|
|
@FeatureNew('add_project_dependencies', '0.63.0')
|
|
@TypedArgs(
|
|
'add_project_dependencies',
|
|
var_types=VarArgInfo(dependencies.Dependency),
|
|
kw_types=[NATIVE_KW, LANGUAGE_KW],
|
|
)
|
|
def func_add_project_dependencies(self, node: mparser.FunctionNode, args: T.Tuple[T.List[dependencies.Dependency]], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
|
|
requested_for_machine = kwargs['native']
|
|
for_machine = self.build.machine_map[requested_for_machine]
|
|
for lang in kwargs['language']:
|
|
if lang not in self.compilers[for_machine]:
|
|
raise InvalidCode(f'add_project_dependencies() called before add_language() for language "{lang}"')
|
|
|
|
for d in dependencies.get_leaf_external_dependencies(args[0]):
|
|
compile_args = list(d.get_compile_args())
|
|
for i in d.get_include_dirs():
|
|
for lang in kwargs['language']:
|
|
comp = self.coredata.compilers[for_machine][lang]
|
|
for idir in i.abs_string_list(self.environment.get_source_dir(), self.environment.get_build_dir()):
|
|
compile_args.extend(comp.get_include_args(idir, i.is_system))
|
|
|
|
self._add_project_arguments(node, self.current_build_project().project_args[requested_for_machine],
|
|
compile_args, kwargs)
|
|
self._add_project_arguments(node, self.current_build_project().project_link_args[requested_for_machine],
|
|
d.get_link_args(), kwargs)
|
|
|
|
def _warn_about_builtin_args(self, args: T.List[str]) -> None:
|
|
# -Wpedantic is deliberately not included, since some people want to use it but not use -Wextra
|
|
# see e.g.
|
|
# https://github.com/mesonbuild/meson/issues/3275#issuecomment-641354956
|
|
# https://github.com/mesonbuild/meson/issues/3742
|
|
warnargs = ('/W1', '/W2', '/W3', '/W4', '/Wall', '-Wall', '-Wextra')
|
|
optargs = ('-O0', '-O2', '-O3', '-Os', '-Oz', '/O1', '/O2', '/Os')
|
|
for arg in args:
|
|
if arg in warnargs:
|
|
mlog.warning(f'Consider using the built-in warning_level option instead of using "{arg}".',
|
|
location=self.current_node)
|
|
elif arg in optargs:
|
|
mlog.warning(f'Consider using the built-in optimization level instead of using "{arg}".',
|
|
location=self.current_node)
|
|
elif arg == '-Werror':
|
|
mlog.warning(f'Consider using the built-in werror option instead of using "{arg}".',
|
|
location=self.current_node)
|
|
elif arg == '-g':
|
|
mlog.warning(f'Consider using the built-in debug option instead of using "{arg}".',
|
|
location=self.current_node)
|
|
# Don't catch things like `-fsanitize-recover`
|
|
elif arg in {'-fsanitize', '/fsanitize'} or arg.startswith(('-fsanitize=', '/fsanitize=')):
|
|
mlog.warning(f'Consider using the built-in option for sanitizers instead of using "{arg}".',
|
|
location=self.current_node)
|
|
elif arg.startswith('-std=') or arg.startswith('/std:'):
|
|
mlog.warning(f'Consider using the built-in option for language standard version instead of using "{arg}".',
|
|
location=self.current_node)
|
|
|
|
def _add_global_arguments(self, node: mparser.FunctionNode, argsdict: T.Dict[Language, T.List[str]],
|
|
args: T.List[str], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
|
|
if self.is_subproject():
|
|
msg = f'Function \'{node.func_name.value}\' cannot be used in subprojects because ' \
|
|
'there is no way to make that reliable.\nPlease only call ' \
|
|
'this if is_subproject() returns false. Alternatively, ' \
|
|
'define a variable that\ncontains your language-specific ' \
|
|
'arguments and add it to the appropriate *_args kwarg ' \
|
|
'in each target.'
|
|
raise InvalidCode(msg)
|
|
frozen = self.project_args_frozen or self.global_args_frozen
|
|
self._add_arguments(node, argsdict, frozen, args, kwargs)
|
|
|
|
def _add_project_arguments(self, node: mparser.FunctionNode, argsdict: T.Dict[Language, T.List[str]],
|
|
args: T.List[str], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
|
|
self._add_arguments(node, argsdict, self.project_args_frozen, args, kwargs)
|
|
|
|
def _add_arguments(self, node: mparser.FunctionNode, argsdict: T.Dict[Language, T.List[str]],
|
|
args_frozen: bool, args: T.List[str], kwargs: 'kwtypes.FuncAddProjectArgs') -> None:
|
|
if args_frozen:
|
|
msg = f'Tried to use \'{node.func_name.value}\' after a build target has been declared.\n' \
|
|
'This is not permitted. Please declare all arguments before your targets.'
|
|
raise InvalidCode(msg)
|
|
|
|
self._warn_about_builtin_args(args)
|
|
|
|
for lang in kwargs['language']:
|
|
argsdict[lang] = argsdict.get(lang, []) + args
|
|
|
|
@noArgsFlattening
|
|
@TypedArgs(
|
|
'environment',
|
|
opt_types=[OptArgInfo((str, list, dict), since='0.52.0')],
|
|
kw_types=[ENV_METHOD_KW, ENV_SEPARATOR_KW.evolve(since='0.62.0')],
|
|
)
|
|
def func_environment(self, node: mparser.FunctionNode, args: T.Tuple[T.Union[None, str, T.List['TYPE_var'], T.Dict[str, 'TYPE_var']]],
|
|
kwargs: kwtypes.FuncEnvironment) -> EnvironmentVariables:
|
|
init = args[0]
|
|
if init is not None:
|
|
FeatureNew.single_use('environment positional arguments', '0.52.0', self.subproject, location=node)
|
|
msg = env_validator(init)
|
|
if msg:
|
|
raise InvalidArguments(f'"environment": {msg}')
|
|
if isinstance(init, dict) and any(i for i in init.values() if isinstance(i, list)):
|
|
FeatureNew.single_use('List of string in dictionary value', '0.62.0', self.subproject, location=node)
|
|
# the validator call above ensured that we have the correct type
|
|
return env_convertor(T.cast('FullEnvInitValueType', init), kwargs['method'], kwargs['separator'])
|
|
return EnvironmentVariables()
|
|
|
|
@TypedArgs('join_paths', var_types=STR_VARG_1)
|
|
def func_join_paths(self, node: mparser.BaseNode, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> str:
|
|
parts = args[0]
|
|
other = os.path.join('', *parts[1:]).replace('\\', '/')
|
|
ret = os.path.join(*parts).replace('\\', '/')
|
|
if isinstance(parts[0], P_OBJ.DependencyVariableString) and '..' not in other:
|
|
return P_OBJ.DependencyVariableString(ret)
|
|
elif isinstance(parts[0], P_OBJ.OptionString):
|
|
name = os.path.join(parts[0].optname, other)
|
|
return P_OBJ.OptionString(ret, name)
|
|
else:
|
|
return ret
|
|
|
|
def run(self) -> None:
|
|
super().run()
|
|
mlog.log('Build targets in project:', mlog.bold(str(self.n_targets)))
|
|
if not self.is_subproject() and self.n_targets != len(self.build.targets):
|
|
mlog.log('Total number of targets:', mlog.bold(str(len(self.build.targets))))
|
|
FeatureNew.report(self.subproject)
|
|
FeatureFixed.report(self.subproject)
|
|
FeatureDeprecated.report(self.subproject)
|
|
FeatureBroken.report(self.subproject)
|
|
if not self.is_subproject():
|
|
self.print_extra_warnings()
|
|
self._print_summary()
|
|
|
|
def print_extra_warnings(self) -> None:
|
|
# TODO cross compilation
|
|
for c in self.coredata.compilers.host.values():
|
|
if c.get_id() == 'clang':
|
|
self.check_clang_asan_lundef()
|
|
break
|
|
|
|
def check_clang_asan_lundef(self) -> None:
|
|
if OptionKey('b_lundef') not in self.coredata.optstore:
|
|
return
|
|
if OptionKey('b_sanitize') not in self.coredata.optstore:
|
|
return
|
|
if (self.coredata.optstore.get_value_for('b_lundef') and
|
|
self.coredata.optstore.get_value_for('b_sanitize')):
|
|
value = self.coredata.optstore.get_value_for('b_sanitize')
|
|
assert isinstance(value, list), 'for mypy'
|
|
mlog.warning(textwrap.dedent(f'''\
|
|
Trying to use {value} sanitizer on Clang with b_lundef.
|
|
This will probably not work.
|
|
Try setting b_lundef to false instead.'''),
|
|
location=self.current_node) # noqa: E128
|
|
|
|
# Check that the indicated file is within the same subproject
|
|
# as we currently are. This is to stop people doing
|
|
# nasty things like:
|
|
#
|
|
# f = files('../../master_src/file.c')
|
|
#
|
|
# Note that this is validated only when the file
|
|
# object is generated. The result can be used in a different
|
|
# subproject than it is defined in (due to e.g. a
|
|
# declare_dependency).
|
|
def validate_within_subproject(self, subdir: str, fname: str) -> None:
|
|
srcdir = self.environment.source_dir
|
|
builddir = self.environment.build_dir
|
|
if isinstance(fname, P_OBJ.DependencyVariableString):
|
|
def validate_installable_file(fpath: Path) -> bool:
|
|
installablefiles: T.Set[Path] = set()
|
|
for d in self.build.data:
|
|
for s in d.sources:
|
|
installablefiles.add(Path(s.absolute_path(srcdir, builddir)))
|
|
installabledirs = [str(Path(srcdir if s.from_source_dir else builddir, s.source_subdir))
|
|
for s in self.build.install_dirs]
|
|
if fpath in installablefiles:
|
|
return True
|
|
for d in installabledirs:
|
|
if str(fpath).startswith(d):
|
|
return True
|
|
return False
|
|
|
|
norm = Path(fname)
|
|
# variables built from a dep.get_variable are allowed to refer to
|
|
# subproject files, as long as they are scheduled to be installed.
|
|
if validate_installable_file(norm):
|
|
return
|
|
|
|
def do_validate_within_subproject(norm: str) -> None:
|
|
if os.path.isdir(norm):
|
|
inputtype = 'directory'
|
|
else:
|
|
inputtype = 'file'
|
|
if is_parent_path(builddir, norm):
|
|
raise BuiltFileByNameError(fname)
|
|
|
|
if not is_parent_path(srcdir, norm):
|
|
# Grabbing files outside the source tree is ok.
|
|
# This is for vendor stuff like:
|
|
#
|
|
# /opt/vendorsdk/src/file_with_license_restrictions.c
|
|
return
|
|
|
|
project_root = os.path.join(srcdir, self.root_subdir)
|
|
if not is_parent_path(project_root, norm):
|
|
name = os.path.basename(norm)
|
|
raise SandboxViolationError(inputtype, name, subproject=False)
|
|
|
|
subproject_dir = os.path.join(project_root, self.subproject_dir)
|
|
if is_parent_path(subproject_dir, norm):
|
|
name = os.path.basename(norm)
|
|
raise SandboxViolationError(inputtype, name)
|
|
|
|
fname = os.path.join(subdir, fname)
|
|
if fname in self.validated_cache:
|
|
return
|
|
|
|
norm = os.path.abspath(os.path.join(srcdir, fname))
|
|
do_validate_within_subproject(norm)
|
|
self.validated_cache.add(fname)
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[str]) -> list[mesonlib.File]: ...
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list['mesonlib.FileOrString']) -> list['mesonlib.File']: ...
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[T.Union[mesonlib.FileOrString, build.CustomTarget, build.CustomTargetIndex]]) -> list[T.Union[mesonlib.File, build.CustomTarget, build.CustomTargetIndex]]: ...
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[T.Union[mesonlib.FileOrString, build.BuildTargetProto]]) -> list[T.Union[mesonlib.File, build.BuildTargetProto]]: ... # type: ignore[overload-overlap]
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[T.Union[str, build.TargetSources]]) -> list[build.TargetSources]: ...
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[kwtypes.CustomTargetInputs]) -> list['CustomTargetSources']: ... # type: ignore[overload-overlap]
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[kwtypes.BuildTargetObjects], # type: ignore[overload-overlap]
|
|
) -> list[T.Union[build.ObjectTypes, build.GeneratedTypes]]: ...
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[T.Union[str, build.TargetSources, build.BuildTargetTypes, build.BothLibraries, build.ExtractedObjects]], # type: ignore[overload-overlap]
|
|
) -> list[T.Union[build.TargetSources, build.BuildTargetTypes, build.BothLibraries, build.ExtractedObjects]]: ...
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[T.Union[str, build.BuildTarget, build.TargetSources]]) -> list[build.BuildTarget | build.TargetSources]: ... # type: ignore[overload-overlap]
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[T.Union[str, build.TargetSources, build.StructuredSources]],
|
|
) -> list[T.Union[build.TargetSources, build.StructuredSources]]: ... # noqa: F811
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list[SourcesVarargsType]) -> list['SourceOutputs']: ...
|
|
|
|
@T.overload
|
|
def source_strings_to_files(self, sources: list['SourceInputs']) -> list['SourceOutputs']: ... # type: ignore[overload-cannot-match]
|
|
|
|
def source_strings_to_files(self,
|
|
sources: T.Union[
|
|
list[str],
|
|
list[mesonlib.File | str],
|
|
list[mesonlib.File | str | build.CustomTarget | build.CustomTargetIndex],
|
|
list[mesonlib.File | str | build.BuildTargetProto],
|
|
list[str | build.TargetSources],
|
|
list[str | build.TargetSources | build.StructuredSources],
|
|
list[str | build.TargetSources | build.BuildTarget],
|
|
list[kwtypes.CustomTargetInputs],
|
|
list[mesonlib.File | str | build.BuildTargetTypes | build.BothLibraries | build.ExtractedObjects | build.GeneratedTypes],
|
|
list[kwtypes.BuildTargetObjects],
|
|
list[SourceInputs],
|
|
list[SourcesVarargsType],
|
|
]
|
|
) -> T.Union[list[mesonlib.File],
|
|
list[mesonlib.File | build.BuildTargetProto],
|
|
list[mesonlib.File | build.CustomTarget | build.CustomTargetIndex],
|
|
list[build.TargetSources],
|
|
list[build.TargetSources | build.StructuredSources],
|
|
list[build.TargetSources | build.BuildTarget],
|
|
list[mesonlib.File | build.BuildTargetTypes | build.BothLibraries | build.ExtractedObjects | build.GeneratedTypes],
|
|
list[build.ObjectTypes | build.GeneratedTypes],
|
|
list[CustomTargetSources],
|
|
list[SourceOutputs]]:
|
|
"""Lower inputs to a list of Targets and Files, replacing any strings.
|
|
|
|
:param sources: A raw (Meson DSL) list of inputs (targets, files, and
|
|
strings)
|
|
:raises InterpreterException: if any of the inputs are of an invalid type
|
|
:return: A list of Targets and Files
|
|
"""
|
|
mesonlib.check_direntry_issues(sources)
|
|
results: list[build.TargetSources | build.BuildTarget | build.ExtractedObjects | build.StructuredSources | Program] = []
|
|
|
|
for s in sources:
|
|
if isinstance(s, str):
|
|
if s.endswith(' '):
|
|
raise MesonException(f'{s!r} ends with a space. This is probably an error.')
|
|
try:
|
|
self.validate_within_subproject(self.subdir, s)
|
|
except BuiltFileByNameError as e:
|
|
# In Meson 2.0 this should just raise
|
|
if not self.relaxed(InterpreterRuleRelaxation.ALLOW_BUILD_DIR_FILE_REFERENCES):
|
|
#raise
|
|
mlog.warning(str(e), location=self.current_node)
|
|
if path_has_root(s):
|
|
# A built File's name must be relative to the build
|
|
# root, otherwise the absolute fname leaks through
|
|
# File.relative_name() and breaks the backend.
|
|
# Use join and relpath together to handle paths without
|
|
# the drive part.
|
|
rel = os.path.relpath(os.path.join(self.environment.build_dir, s),
|
|
self.environment.build_dir)
|
|
results.append(mesonlib.File.from_built_relative(rel))
|
|
else:
|
|
results.append(mesonlib.File.from_built_file(self.current_build_dir(), s))
|
|
continue
|
|
except SandboxViolationError:
|
|
if not self.relaxed(InterpreterRuleRelaxation.ALLOW_SANDBOX_VIOLATION):
|
|
raise
|
|
results.append(mesonlib.File.from_source_file(self.environment.source_dir, self.subdir, s))
|
|
elif isinstance(s, (mesonlib.File, build.GeneratedList, Program,
|
|
build.BuildTarget, build.CustomTargetIndex, build.CustomTarget,
|
|
build.ExtractedObjects, build.StructuredSources)):
|
|
results.append(s)
|
|
else:
|
|
raise InterpreterException(f'Source item is {s!r} instead of '
|
|
'string or File-type object')
|
|
return results # type: ignore[return-value]
|
|
|
|
def source_string_to_file(self, source: str | mesonlib.File | build.CustomTarget | build.CustomTargetIndex | None) -> mesonlib.File | build.CustomTarget | build.CustomTargetIndex:
|
|
if source is None:
|
|
return None
|
|
return self.source_strings_to_files([source])[0]
|
|
|
|
def validate_forbidden_targets(self, name: str, in_root: bool) -> None:
|
|
if name.startswith('meson-internal__'):
|
|
raise InvalidArguments("Target names starting with 'meson-internal__' are reserved "
|
|
"for Meson's internal use. Please rename.")
|
|
if name.startswith('meson-') and '.' not in name:
|
|
raise InvalidArguments("Target names starting with 'meson-' and without a file extension "
|
|
"are reserved for Meson's internal use. Please rename.")
|
|
if name in coredata.FORBIDDEN_TARGET_NAMES:
|
|
if in_root:
|
|
raise InvalidArguments(f"Target name '{name}' is reserved for Meson's "
|
|
"internal use. Please rename.")
|
|
else:
|
|
FeatureNew.single_use(f"Target name '{name}' reserved in the root build directory, but allowed in subdirectories",
|
|
'1.12.0', self.subproject, location=self.current_node)
|
|
|
|
def add_target(self, name: str, tobj: build.Target) -> None:
|
|
if self.backend.name == 'none':
|
|
raise InterpreterException('Install-only backend cannot generate target rules, try using `--backend=ninja`.')
|
|
if name == '':
|
|
raise InterpreterException('Target name must not be empty.')
|
|
if name.strip() == '':
|
|
raise InterpreterException('Target name must not consist only of whitespace.')
|
|
if has_path_sep(name):
|
|
pathseg = os.path.join(self.subdir, os.path.split(name)[0])
|
|
if os.path.exists(os.path.join(self.source_root, pathseg)):
|
|
raise InvalidArguments(textwrap.dedent(f'''\
|
|
Target "{name}" has a path segment pointing to directory "{pathseg}". This is an error.
|
|
To define a target that builds in that directory you must define it
|
|
in the meson.build file in that directory.
|
|
'''))
|
|
|
|
# Keep track of the number of targets added by the (sub)project
|
|
# being interpreted. For informational purposes only.
|
|
self.n_targets += 1
|
|
|
|
# Make sure build_subdir doesn't exist in the source tree and
|
|
# doesn't contain ..
|
|
build_subdir = tobj.get_build_subdir()
|
|
self.validate_build_subdir(build_subdir, name)
|
|
|
|
subdir = tobj.get_builddir()
|
|
self.validate_forbidden_targets(name, not subdir)
|
|
|
|
# To permit an executable and a shared library to have the
|
|
# same name, such as "foo.exe" and "libfoo.a".
|
|
idname = tobj.get_id()
|
|
namedir = (name, subdir)
|
|
|
|
if idname in self.build.targets:
|
|
raise InvalidCode(f'Tried to create target "{name}", but a target of that name already exists.')
|
|
|
|
if isinstance(tobj, build.Executable) and namedir in self.build.targetnames:
|
|
FeatureNew.single_use(f'multiple executables with the same name, "{tobj.name}", but different suffixes in the same directory',
|
|
'1.3.0', self.subproject, location=self.current_node)
|
|
|
|
if isinstance(tobj, build.BuildTarget):
|
|
# add_languages mutates the `args` parameter, so we need to copy here
|
|
self.add_languages(tobj.missing_languages.copy(), True, tobj.for_machine)
|
|
tobj.process_compilers_late()
|
|
self.add_stdlib_info(tobj)
|
|
|
|
if tobj.uses_cython():
|
|
for gensrc in tobj.get_generated_sources():
|
|
# a generator producing a .pyx has always worked; feeding
|
|
# anything else from a generator into Cython triggered a bug
|
|
# in the ninja backend and was effectively a new feature.
|
|
if isinstance(gensrc, build.GeneratedList) and \
|
|
any(not o.endswith('.pyx') for o in gensrc.get_outputs()):
|
|
FeatureNew.single_use('Generators for non-.pyx Cython sources',
|
|
'1.12.0', self.subproject, location=self.current_node)
|
|
break
|
|
|
|
self.build.targets[idname] = tobj
|
|
# Only need to add executables to this set
|
|
if isinstance(tobj, build.Executable):
|
|
self.build.targetnames.update([namedir])
|
|
if idname not in self.coredata.target_guids:
|
|
self.coredata.target_guids[idname] = str(uuid.uuid4()).upper()
|
|
|
|
@FeatureNew('both_libraries', '0.46.0')
|
|
def build_both_libraries(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], kwargs: kwtypes.Library) -> build.BothLibraries:
|
|
shared_lib = self.build_target(node, args, kwargs, build.SharedLibrary, shared_library_only=False)
|
|
static_lib = self.build_target(node, args, kwargs, build.StaticLibrary)
|
|
preferred_library = self.coredata.optstore.get_value_for(OptionKey('default_both_libraries', subproject=self.subproject))
|
|
assert isinstance(preferred_library, str), 'for mypy'
|
|
if preferred_library == 'auto':
|
|
preferred_library = self.coredata.optstore.get_value_for(OptionKey('default_library', subproject=self.subproject))
|
|
assert isinstance(preferred_library, str), 'for mypy'
|
|
if preferred_library == 'both':
|
|
preferred_library = 'shared'
|
|
assert preferred_library in {'shared', 'static'}
|
|
preferred_library = T.cast('Literal["static", "shared"]', preferred_library)
|
|
|
|
if self.backend.name == 'xcode':
|
|
# Xcode is a bit special in that you can't (at least for the moment)
|
|
# form a library only from object file inputs. The simple but inefficient
|
|
# solution is to use the sources directly. This will lead to them being
|
|
# built twice. This is unfortunate and slow, but at least it works.
|
|
# Feel free to submit patches to get this fixed if it is an
|
|
# issue for you.
|
|
reuse_object_files = False
|
|
elif shared_lib.uses_rust():
|
|
# FIXME: rustc supports generating both libraries in a single invocation,
|
|
# but for now compile twice.
|
|
reuse_object_files = False
|
|
elif any(k.endswith(('static_args', 'shared_args')) and v for k, v in kwargs.items()):
|
|
# Ensure not just the keyword arguments exist, but that they are non-empty.
|
|
reuse_object_files = False
|
|
else:
|
|
reuse_object_files = static_lib.pic
|
|
|
|
if reuse_object_files:
|
|
# Replace sources with objects from the shared library to avoid
|
|
# building them twice. We post-process the static library instead of
|
|
# removing sources from args because sources could also come from
|
|
# any InternalDependency, see BuildTarget.add_deps().
|
|
static_lib.objects.append(build.ExtractedObjects(shared_lib, shared_lib.sources, shared_lib.generated, []))
|
|
static_lib.sources = []
|
|
static_lib.generated = []
|
|
# Compilers with no corresponding sources confuses the backend.
|
|
# Keep only compilers used for linking
|
|
static_lib.compilers = {k: v for k, v in static_lib.compilers.items() if k in compilers.clink_langs}
|
|
|
|
# Cross reference them to implement as_shared() and as_static() methods.
|
|
shared_lib.set_static(static_lib)
|
|
static_lib.set_shared(shared_lib)
|
|
|
|
return build.BothLibraries(shared_lib, static_lib, preferred_library)
|
|
|
|
def build_library(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], kwargs: kwtypes.Library) -> build.SharedLibrary | build.BothLibraries | build.StaticLibrary:
|
|
default_library = self.coredata.optstore.get_value_for(OptionKey('default_library', subproject=self.subproject))
|
|
assert isinstance(default_library, str), 'for mypy'
|
|
if default_library == 'shared':
|
|
# Intentionally pass shared_library_only=False so that dependencies
|
|
# end up in Requires.private. Many libraries that refer to their
|
|
# dependencies' in their headers expect this so that those dependencies
|
|
# are added to the output of 'pkgconfig --cflags'.
|
|
return self.build_target(node, args, T.cast('kwtypes.SharedLibrary', kwargs), build.SharedLibrary, shared_library_only=False)
|
|
elif default_library == 'static':
|
|
return self.build_target(node, args, T.cast('kwtypes.StaticLibrary', kwargs), build.StaticLibrary)
|
|
elif default_library == 'both':
|
|
return self.build_both_libraries(node, args, kwargs)
|
|
else:
|
|
raise InterpreterException(f'Unknown default_library value: {default_library}.')
|
|
|
|
def __convert_file_args(self, raw: T.List[mesonlib.FileOrString]) -> T.Tuple[T.List[mesonlib.File], T.List[str]]:
|
|
"""Convert raw target arguments from File | str to File.
|
|
|
|
This removes files from the command line and replaces them with string
|
|
values, but adds the files to depends list
|
|
|
|
:param raw: the raw arguments
|
|
:return: A tuple of file dependencies and raw arguments
|
|
"""
|
|
depend_files: T.List[mesonlib.File] = []
|
|
args: T.List[str] = []
|
|
build_to_source = mesonlib.relpath(self.environment.get_source_dir(),
|
|
self.environment.get_build_dir())
|
|
|
|
for a in raw:
|
|
if isinstance(a, mesonlib.File):
|
|
depend_files.append(a)
|
|
args.append(a.rel_to_builddir(build_to_source))
|
|
else:
|
|
args.append(a)
|
|
|
|
return depend_files, args
|
|
|
|
def __process_language_args(self, kwargs: kwtypes.BaseBuildTarget,
|
|
) -> T.Tuple[T.DefaultDict[Language, T.List[str]], T.List[mesonlib.File]]:
|
|
"""Convert split language args into a combined dictionary.
|
|
|
|
The Meson DSL takes arguments in the form `<lang>_args : args`, but in the
|
|
build layer we store these in a single dictionary as `{<lang>: args}`.
|
|
This function extracts the arguments from the DSL format and prepares
|
|
them for the IR.
|
|
"""
|
|
deps: T.List[mesonlib.File] = []
|
|
new_args: T.DefaultDict[Language, T.List[str]] = collections.defaultdict(list)
|
|
|
|
for l in compilers.all_languages:
|
|
deps, args = self.__convert_file_args(kwargs[f'{l}_args']) # type: ignore[literal-required]
|
|
new_args[l] = args
|
|
deps.extend(deps)
|
|
return new_args, deps
|
|
|
|
@staticmethod
|
|
def _handle_rust_abi(abi: T.Optional[Literal['c', 'rust']],
|
|
crate_type: T.Optional[build.RustCrateType],
|
|
default_rust_type: build.RustCrateType,
|
|
default_c_type: build.RustCrateType, typename: str,
|
|
extra_valid_types: T.Optional[T.Set[build.RustCrateType]] = None,
|
|
) -> build.RustCrateType:
|
|
"""Handle the interactions between the rust_abi and rust_crate_type keyword arguments.
|
|
|
|
:param abi: Is this using Rust ABI or C ABI
|
|
:param crate_type: Is there an explicit crate type set
|
|
:param default_rust_type: The default crate type to use for Rust ABI
|
|
:param default_c_type: the default crate type to use for C ABI
|
|
:param typename: The name of the type this argument is for
|
|
:param extra_valid_types: additional valid crate types, defaults to None
|
|
:raises InvalidArguments: If the crate_type argument is set, but not valid
|
|
:raises InvalidArguments: If both crate_type and abi are set
|
|
:return: The finalized crate type
|
|
"""
|
|
if abi is not None:
|
|
if crate_type is not None:
|
|
raise InvalidArguments('rust_abi and rust_crate_type are mutually exclusive')
|
|
crate_type = default_rust_type if abi == 'rust' else default_c_type
|
|
elif crate_type is not None:
|
|
if crate_type == 'lib':
|
|
crate_type = default_rust_type
|
|
valid_types = {default_rust_type, default_c_type} | (extra_valid_types or set())
|
|
if crate_type not in valid_types:
|
|
choices = ", " .join(f'"{c}"' for c in sorted(valid_types))
|
|
raise InvalidArguments(f'Crate type for {typename} must be one of {choices}, but got "{crate_type}"')
|
|
else:
|
|
crate_type = default_rust_type
|
|
return crate_type
|
|
|
|
def _exe_to_shlib_kwargs(self, kwargs: kwtypes.Executable) -> kwtypes.SharedLibrary:
|
|
nkwargs = T.cast('kwtypes.SharedLibrary', kwargs.copy())
|
|
for exe_kwarg in EXCLUSIVE_EXECUTABLE_KWS:
|
|
del nkwargs[exe_kwarg.name] # type: ignore[misc]
|
|
for sh_kwarg in SHARED_LIB_KWS:
|
|
nkwargs.setdefault(sh_kwarg.name, sh_kwarg.default) # type: ignore[misc]
|
|
nkwargs['rust_abi'] = None
|
|
nkwargs['rust_crate_type'] = 'cdylib'
|
|
return nkwargs
|
|
|
|
def __convert_build_target_base_kwargs(self, kwargs: kwtypes.BuildTarget, final: build.BuildTargetKeywordArguments) -> None:
|
|
"""Convert shared arguments for BuildTargets to the Build layer form.
|
|
|
|
This takes the raw DSL form, and builds a new dict in the form that the BuildTarget expects
|
|
|
|
:param kwargs: The arguments in raw DSL form
|
|
:param final: A new dictionary to fill in with the Build layer
|
|
:raises InvalidArguments: If one the PCH files doesn't exist
|
|
"""
|
|
# copy common arguments directly
|
|
for arg in ('build_by_default', 'build_rpath', 'build_subdir', 'c_pch',
|
|
'cpp_pch', 'd_debug', 'd_module_versions', 'd_unittest',
|
|
'dependencies', 'gnu_symbol_visibility', 'install',
|
|
'install_mode', 'install_rpath',
|
|
'implicit_include_directories', 'link_args', 'link_early_args',
|
|
'link_language', 'link_with', 'link_whole', 'name_prefix',
|
|
'name_suffix', 'native', 'resources', 'vala_header',
|
|
'vala_gir', 'vala_vapi', 'swift_interoperability_mode',
|
|
'swift_module_name', 'rust_crate_type',
|
|
'rust_dependency_map', 'override_options'):
|
|
final[arg] = kwargs[arg]
|
|
|
|
# this is not accessable from the DSL, so we need to initialize it
|
|
# ourselves
|
|
final['depend_files'] = []
|
|
|
|
# Check for non-existant PCH files
|
|
missing: T.List[str] = []
|
|
for each in itertools.chain(kwargs['c_pch'] or [], kwargs['cpp_pch'] or []):
|
|
if each is not None:
|
|
if not os.path.isfile(os.path.join(self.environment.source_dir, self.subdir, each)):
|
|
missing.append(os.path.join(self.subdir, each))
|
|
if missing:
|
|
raise InvalidArguments('The following PCH files do not exist: {}'.format(', '.join(missing)))
|
|
|
|
final['link_depends'] = self.source_strings_to_files(kwargs['link_depends'])
|
|
final['install_tag'] = [kwargs['install_tag']]
|
|
final['extra_files'] = mesonlib.unique_list(self.source_strings_to_files(kwargs['extra_files']))
|
|
|
|
# Convert into IncludeDirs objects
|
|
final['include_directories'] = self.extract_incdirs(kwargs['include_directories'])
|
|
final['embed_directories'] = self.extract_incdirs(kwargs['embed_directories'])
|
|
final['d_import_dirs'] = self.extract_incdirs(kwargs['d_import_dirs'], True)
|
|
|
|
# Convert language arguments
|
|
lang_args, deps = self.__process_language_args(kwargs)
|
|
final['language_args'] = lang_args
|
|
final['depend_files'].extend(deps)
|
|
|
|
# Rewrite `install_dir : [main, vala_header, vala_vapi, vala_gir]`
|
|
# Into split arguments
|
|
vala_keys = ('install_vala_header', 'install_vala_vapi', 'install_vala_gir')
|
|
if len(kwargs['install_dir']) > 1:
|
|
if any(kwargs[k] is not None for k in vala_keys): # type: ignore[literal-required]
|
|
raise InvalidArguments('Passing install_vala_* and more than one argument to install_dir are mutually exclusive')
|
|
|
|
install_dirs = kwargs['install_dir'].copy()
|
|
install_dir = install_dirs.pop(0)
|
|
|
|
for key in vala_keys:
|
|
action = install_dirs.pop(0) if install_dirs else False
|
|
if isinstance(action, bool):
|
|
final[key] = action # type: ignore[literal-required]
|
|
else:
|
|
final[key] = True # type: ignore[literal-required]
|
|
final[f'{key}_dir'] = action # type: ignore[literal-required]
|
|
final['install_dir'] = [install_dir]
|
|
|
|
# This was previously allowed, and tested, so we need to allow it to keep working.
|
|
if final['vala_gir'] is None and final['install_vala_gir']:
|
|
final['install_vala_gir'] = False
|
|
else:
|
|
for key in vala_keys:
|
|
action = kwargs[key] # type: ignore[literal-required]
|
|
if action is None:
|
|
action = False
|
|
if isinstance(action, bool):
|
|
final[key] = action # type: ignore[literal-required]
|
|
else:
|
|
final[key] = True # type: ignore[literal-required]
|
|
final[f'{key}_dir'] = action # type: ignore[literal-required]
|
|
final['install_dir'] = [kwargs['install_dir'][0] if kwargs['install_dir'] else True]
|
|
|
|
def __convert_executable_kwargs(self, node: mparser.BaseNode, kwargs: kwtypes.Executable) -> build.ExecutableKeywordArguments:
|
|
"""Convert Executable arguments from DSL form to the build layer form.
|
|
|
|
:param node: The Node being evaluated
|
|
:param kwargs: The DSL keyword arguments
|
|
:raises InvalidArguments: If both gui_app and win_subsystem are set
|
|
:raises InvalidArguments: If implib is false and export_dynamic is true
|
|
:raises InvalidArguments: If the rust_crate_type is set to anything except bin
|
|
:return: The keyword arguments as the Build layer expects
|
|
"""
|
|
final: build.ExecutableKeywordArguments = {}
|
|
self.__convert_build_target_base_kwargs(kwargs, final)
|
|
|
|
# Exe exclusive arguments
|
|
for exe_arg in ('android_exe_type', 'pie'):
|
|
final[exe_arg] = kwargs[exe_arg]
|
|
final['vs_module_defs'] = self.source_string_to_file(kwargs['vs_module_defs'])
|
|
|
|
# rewrite `gui_app` to `win_subsystem`
|
|
if kwargs['gui_app'] is not None:
|
|
if kwargs['win_subsystem'] is not None:
|
|
raise InvalidArguments.from_node(
|
|
'Executable got both "gui_app", and "win_subsystem" arguments, which are mutually exclusive',
|
|
node=node)
|
|
final['win_subsystem'] = 'windows' if kwargs['gui_app'] else 'console'
|
|
elif kwargs['win_subsystem'] is not None:
|
|
final['win_subsystem'] = kwargs['win_subsystem']
|
|
else:
|
|
final['win_subsystem'] = 'console'
|
|
|
|
# handle interactions between implib and export_dynamic
|
|
if kwargs['implib']:
|
|
if kwargs['export_dynamic'] is False:
|
|
FeatureDeprecated.single_use(
|
|
'implib overrides explicit export_dynamic off', '1.3.0', self.subproject,
|
|
'Do not set ths if want export_dynamic disabled if implib is enabled',
|
|
location=node)
|
|
kwargs['export_dynamic'] = True
|
|
elif kwargs['export_dynamic']:
|
|
if kwargs['implib'] is False:
|
|
raise InvalidArguments.from_node(
|
|
'"implib" keyword" must not be false if "export_dynamic" is set and not false.',
|
|
node=node)
|
|
if kwargs['export_dynamic'] is None:
|
|
kwargs['export_dynamic'] = False
|
|
final['export_dynamic'] = kwargs['export_dynamic']
|
|
|
|
if isinstance(kwargs['implib'], bool):
|
|
final['implib'] = None
|
|
else:
|
|
final['implib'] = kwargs['implib']
|
|
|
|
# Handle executable speicific rust_crate_type
|
|
if kwargs['rust_crate_type'] not in {None, 'bin'}:
|
|
raise InvalidArguments.from_node(
|
|
'Crate type for executable must be "bin"', node=node)
|
|
final['rust_crate_type'] = 'bin'
|
|
|
|
return final
|
|
|
|
def __convert_static_library_kwargs(self, node: mparser.BaseNode, kwargs: kwtypes.StaticLibrary) -> build.StaticLibraryKeywordArguments:
|
|
"""Convert StaticLibrary arguments to the Build format.
|
|
|
|
:param node: The Node currently be evaluated.
|
|
:param kwargs: The DSL form of the keyword arguments.
|
|
:return: The arguments in Build layer format.
|
|
"""
|
|
final: build.StaticLibraryKeywordArguments = {}
|
|
self.__convert_build_target_base_kwargs(kwargs, final)
|
|
|
|
for arg in ('pic', 'prelink'):
|
|
final[arg] = kwargs[arg]
|
|
|
|
for lang in compilers.all_languages - {'java'}:
|
|
deps, args = self.__convert_file_args(kwargs.get(f'{lang}_static_args', [])) # type: ignore[arg-type]
|
|
final['language_args'][lang].extend(args)
|
|
final['depend_files'].extend(deps)
|
|
final['rust_crate_type'] = self._handle_rust_abi(
|
|
kwargs['rust_abi'], kwargs['rust_crate_type'], 'rlib', 'staticlib',
|
|
build.StaticLibrary.typename)
|
|
|
|
return final
|
|
|
|
def __convert_shared_library_kwargs(self, node: mparser.BaseNode, kwargs: kwtypes.SharedLibrary) -> build.SharedLibraryKeywordArguments:
|
|
"""Convert SharedLibrary arguments to the Build format.
|
|
|
|
:param node: The Node currently be evaluated.
|
|
:param kwargs: The DSL form of the keyword arguments.
|
|
:return: The arguments in Build layer format.
|
|
"""
|
|
final: build.SharedLibraryKeywordArguments = {}
|
|
self.__convert_build_target_base_kwargs(kwargs, final)
|
|
|
|
for arg in ('version', 'soversion', 'darwin_versions', 'shortname'):
|
|
final[arg] = kwargs[arg]
|
|
final['vs_module_defs'] = self.source_string_to_file(kwargs['vs_module_defs'])
|
|
|
|
for lang in compilers.all_languages - {'java'}:
|
|
deps, args = self.__convert_file_args(kwargs.get(f'{lang}_shared_args', [])) # type: ignore[arg-type]
|
|
final['language_args'][lang].extend(args)
|
|
final['depend_files'].extend(deps)
|
|
final['rust_crate_type'] = self._handle_rust_abi(
|
|
kwargs['rust_abi'], kwargs['rust_crate_type'], 'dylib', 'cdylib',
|
|
build.SharedLibrary.typename, extra_valid_types={'proc-macro'})
|
|
|
|
final['win_subsystem'] = kwargs['win_subsystem'] or 'console'
|
|
|
|
return final
|
|
|
|
def __convert_shared_module_kwargs(self, node: mparser.BaseNode, kwargs: kwtypes.SharedModule) -> build.SharedModuleKeywordArguments:
|
|
"""Convert SharedModule arguments to the Build format.
|
|
|
|
:param node: The Node currently be evaluated.
|
|
:param kwargs: The DSL form of the keyword arguments.
|
|
:return: The arguments in Build layer format.
|
|
"""
|
|
final: build.SharedModuleKeywordArguments = {}
|
|
self.__convert_build_target_base_kwargs(kwargs, final)
|
|
|
|
final['vs_module_defs'] = self.source_string_to_file(kwargs['vs_module_defs'])
|
|
|
|
for lang in compilers.all_languages - {'java'}:
|
|
deps, args = self.__convert_file_args(kwargs.get(f'{lang}_shared_args', [])) # type: ignore[arg-type]
|
|
final['language_args'][lang].extend(args)
|
|
final['depend_files'].extend(deps)
|
|
final['rust_crate_type'] = self._handle_rust_abi(
|
|
kwargs['rust_abi'], kwargs['rust_crate_type'], 'dylib', 'cdylib',
|
|
build.SharedModule.typename, extra_valid_types={'proc-macro'})
|
|
|
|
final['win_subsystem'] = kwargs['win_subsystem'] or 'console'
|
|
|
|
return final
|
|
|
|
def __convert_jar_kwargs(self, node: mparser.BaseNode, kwargs: kwtypes.Jar) -> build.JarKeywordArguments:
|
|
"""Convert Jar arguments to the Build format.
|
|
|
|
:param node: The Node currently be evaluated.
|
|
:param kwargs: The DSL form of the keyword arguments.
|
|
:return: The arguments in Build layer format.
|
|
"""
|
|
final: build.JarKeywordArguments = {}
|
|
|
|
# copy common arguments directly
|
|
for arg in ('build_by_default', 'build_subdir', 'dependencies',
|
|
'install', 'install_mode', 'implicit_include_directories',
|
|
'link_with', 'java_args', 'java_resources', 'main_class',
|
|
'native'):
|
|
final[arg] = kwargs[arg]
|
|
|
|
# this is not accessable from the DSL, so we need to initialize it
|
|
# ourselves
|
|
final['depend_files'] = []
|
|
|
|
# The build layer and interpreter don't agree here, and it's not clear
|
|
# that we're hanlding this correctly
|
|
final['install_dir'] = kwargs['install_dir'] # type: ignore[typeddict-item]
|
|
final['install_tag'] = [kwargs['install_tag']]
|
|
final['extra_files'] = mesonlib.unique_list(self.source_strings_to_files(kwargs['extra_files']))
|
|
|
|
# Convert into IncludeDirs objects
|
|
final['include_directories'] = self.extract_incdirs(kwargs['include_directories'])
|
|
|
|
# Convert language arguments
|
|
lang_args, deps = self.__process_language_args(kwargs)
|
|
final['language_args'] = lang_args
|
|
final['depend_files'].extend(deps)
|
|
|
|
return final
|
|
|
|
@T.overload
|
|
def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.Executable, targetclass: T.Type[build.Executable]) -> build.Executable: ...
|
|
|
|
@T.overload
|
|
def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.StaticLibrary, targetclass: T.Type[build.StaticLibrary]) -> build.StaticLibrary: ...
|
|
|
|
@T.overload
|
|
def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.SharedModule, targetclass: T.Type[build.SharedModule]) -> build.SharedModule: ...
|
|
|
|
@T.overload
|
|
def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.SharedLibrary, targetclass: T.Type[build.SharedLibrary],
|
|
shared_library_only: bool = True) -> build.SharedLibrary: ...
|
|
|
|
@T.overload
|
|
def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: kwtypes.Jar, targetclass: T.Type[build.Jar]) -> build.Jar: ...
|
|
|
|
def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType],
|
|
kwargs: T.Union[kwtypes.Executable, kwtypes.StaticLibrary, kwtypes.SharedLibrary, kwtypes.SharedModule, kwtypes.Jar],
|
|
targetclass: T.Type[T.Union[build.Executable, build.StaticLibrary, build.SharedModule, build.SharedLibrary, build.Jar]],
|
|
shared_library_only: bool = True
|
|
) -> T.Union[build.Executable, build.StaticLibrary, build.SharedModule, build.SharedLibrary, build.Jar]:
|
|
if targetclass not in {build.Executable, build.SharedLibrary, build.SharedModule, build.StaticLibrary, build.Jar}:
|
|
mlog.debug('Unknown target type:', str(targetclass))
|
|
raise RuntimeError('Unreachable code')
|
|
|
|
# preserved for global and project arguments
|
|
orig_for_machine = kwargs['native']
|
|
self.apply_machine_map_to_kwargs(kwargs)
|
|
for_machine = kwargs['native']
|
|
|
|
if self.environment.is_cross_build() and kwargs.get('rust_crate_type') == 'proc-macro':
|
|
# Silently force to native because that's the only sensible value
|
|
# and rust_crate_type is deprecated any way.
|
|
for_machine = MachineChoice.BUILD
|
|
|
|
if targetclass is build.Executable and kwargs.get('android_exe_type') == 'application':
|
|
m = self.environment.machines[for_machine]
|
|
if m.is_android():
|
|
# Mypy can't figure this out for some reason
|
|
kwargs = T.cast('kwtypes.Executable', kwargs)
|
|
return self.build_target(node, args, self._exe_to_shlib_kwargs(kwargs), build.SharedLibrary)
|
|
|
|
# Because who owns this isn't clear
|
|
kwargs = kwargs.copy()
|
|
name, raw_sources = args
|
|
# Avoid mutating, since there could be other references to sources
|
|
raw_sources = raw_sources + T.cast('SourcesVarargsType', kwargs['sources'])
|
|
if any(isinstance(s, build.BuildTarget) for s in raw_sources):
|
|
FeatureBroken.single_use('passing references to built targets as a source file', '1.1.0', self.subproject,
|
|
'Consider using `link_with` or `link_whole` if you meant to link, or dropping them as otherwise they are ignored.',
|
|
node)
|
|
if any(isinstance(s, build.ExtractedObjects) for s in raw_sources):
|
|
FeatureBroken.single_use('passing object files as sources', '1.1.0', self.subproject,
|
|
'Pass these to the `objects` keyword instead, they are ignored when passed as sources.',
|
|
node)
|
|
# Go ahead and drop these here, since they're only allowed through for
|
|
# backwards compatibility anyway
|
|
sources = self.source_strings_to_files([
|
|
s for s in raw_sources if not isinstance(s, (build.BuildTarget, build.ExtractedObjects))])
|
|
|
|
objs = self.source_strings_to_files(kwargs['objects'])
|
|
|
|
srcs: T.List[SourceOutputs] = []
|
|
struct: T.Optional[build.StructuredSources] = build.StructuredSources()
|
|
for s in sources:
|
|
if isinstance(s, build.StructuredSources):
|
|
struct = struct + s
|
|
else:
|
|
srcs.append(s)
|
|
|
|
if not struct:
|
|
struct = None
|
|
else:
|
|
# Validate that we won't end up with two outputs with the same name.
|
|
# i.e, don't allow:
|
|
# [structured_sources('foo/bar.rs'), structured_sources('bar/bar.rs')]
|
|
for v in struct.sources.values():
|
|
outputs: T.Set[str] = set()
|
|
for f in v:
|
|
o: T.List[str]
|
|
if isinstance(f, mesonlib.File):
|
|
o = [f.fname]
|
|
else:
|
|
o = f.get_outputs()
|
|
conflicts = outputs.intersection(o)
|
|
if conflicts:
|
|
raise InvalidArguments.from_node(
|
|
f"Conflicting sources in structured sources: {', '.join(sorted(conflicts))}",
|
|
node=node)
|
|
outputs.update(o)
|
|
|
|
if targetclass is not build.Jar:
|
|
self.check_for_jar_sources(sources, targetclass)
|
|
self.check_for_jar_link_with(kwargs.get('link_with', []), targetclass)
|
|
|
|
target: build.BuildTarget
|
|
if targetclass is build.Executable:
|
|
nkwargs = self.__convert_executable_kwargs(
|
|
node, T.cast('kwtypes.Executable', kwargs))
|
|
target = build.Executable(name, self.subdir, orig_for_machine, srcs, struct, objs,
|
|
self.environment, self.compilers[for_machine], self.current_build_project(), nkwargs)
|
|
elif targetclass is build.StaticLibrary:
|
|
nkwargs = self.__convert_static_library_kwargs(
|
|
node, T.cast('kwtypes.StaticLibrary', kwargs))
|
|
target = build.StaticLibrary(name, self.subdir, orig_for_machine, srcs, struct, objs,
|
|
self.environment, self.compilers[for_machine], self.current_build_project(), nkwargs)
|
|
elif targetclass is build.SharedLibrary:
|
|
nkwargs = self.__convert_shared_library_kwargs(
|
|
node, T.cast('kwtypes.SharedLibrary', kwargs))
|
|
target = build.SharedLibrary(name, self.subdir, orig_for_machine, srcs, struct, objs,
|
|
self.environment, self.compilers[for_machine], self.current_build_project(), nkwargs)
|
|
target.shared_library_only = shared_library_only
|
|
elif targetclass is build.SharedModule:
|
|
nkwargs = self.__convert_shared_module_kwargs(
|
|
node, T.cast('kwtypes.SharedModule', kwargs))
|
|
target = build.SharedModule(name, self.subdir, orig_for_machine, srcs, struct, objs,
|
|
self.environment, self.compilers[for_machine], self.current_build_project(), nkwargs)
|
|
target.shared_library_only = shared_library_only
|
|
else:
|
|
nkwargs = self.__convert_jar_kwargs(
|
|
node, T.cast('kwtypes.Jar', kwargs))
|
|
target = build.Jar(name, self.subdir, orig_for_machine, srcs, struct, objs,
|
|
self.environment, self.compilers[for_machine], self.current_build_project(), nkwargs)
|
|
|
|
if objs and target.uses_rust():
|
|
FeatureNew.single_use('objects in Rust targets', '1.8.0', self.subproject)
|
|
|
|
self.add_target(name, target)
|
|
self.project_args_frozen = True
|
|
return target
|
|
|
|
def add_stdlib_info(self, target: build.BuildTarget) -> None:
|
|
for l in target.compilers.keys():
|
|
dep = self.build.stdlibs[target.for_machine].get(l, None)
|
|
if dep:
|
|
target.add_deps([dep])
|
|
|
|
def check_for_jar_sources(self, sources: T.Union[T.List[str], T.Sequence[T.Union[build.TargetSources, build.StructuredSources]]],
|
|
targetclass: T.Type[build.BuildTarget]) -> None:
|
|
for s in sources:
|
|
if isinstance(s, (str, mesonlib.File)) and compilers.is_java(s):
|
|
raise InvalidArguments(f'Build target of type "{targetclass.typename}" cannot build java source: "{s}". Use "{build.Jar.typename}" instead.')
|
|
elif isinstance(s, build.StructuredSources):
|
|
self.check_for_jar_sources(s.as_list(), targetclass)
|
|
elif isinstance(s, (build.GeneratedList, build.CustomTarget, build.CustomTargetIndex)):
|
|
self.check_for_jar_sources(s.get_outputs(), targetclass)
|
|
|
|
def check_for_jar_link_with(self, link_with: T.Sequence[object], targetclass: T.Type[build.BuildTarget]) -> None:
|
|
for t in link_with:
|
|
if isinstance(t, build.Jar):
|
|
raise InvalidArguments(f'Build target of type "{targetclass.typename}" cannot link with jar target "{t.name}". '
|
|
f'Jar targets can only be linked into other jar targets.')
|
|
|
|
def is_subproject(self) -> bool:
|
|
return self.subproject != ''
|
|
|
|
def current_build_dir(self) -> str:
|
|
"""Return the directory of the current meson.build in the build tree, relative to the build directory.
|
|
|
|
Unlike self.subdir, which is relative to the source tree, this has a different
|
|
prefix for subprojects that are built for the build machine.
|
|
"""
|
|
project = self.current_build_project()
|
|
# self.subdir is always inside the directory of the subproject.
|
|
return project.builddir + self.subdir[len(project.subdir):]
|
|
|
|
@TypedArgs(
|
|
'set_variable',
|
|
pos_types=[
|
|
STR_PARG.evolve(validator=lambda x: f'Invalid variable name: "{x}"' if mparser.IDENT_RE.fullmatch(x) is None else None),
|
|
PosArgInfo((object, DefaultObject), accept_second_level_holder=True),
|
|
],
|
|
)
|
|
@noArgsFlattening
|
|
def func_set_variable(self, node: mparser.BaseNode, args: T.Tuple[str, TYPE_var | InterpreterObject], kwargs: 'TYPE_kwargs') -> None:
|
|
self.set_variable(*args, holderify=True)
|
|
|
|
@TypedArgs(
|
|
'get_variable',
|
|
pos_types=[PosArgInfo((str, Disabler, DefaultObject))],
|
|
opt_types=[OBJ_OARG],
|
|
)
|
|
@noArgsFlattening
|
|
@unholder_return
|
|
def func_get_variable(self, node: mparser.BaseNode, args: T.Tuple[T.Union[str, Disabler], T.Optional[TYPE_var | InterpreterObject]],
|
|
kwargs: 'TYPE_kwargs') -> TYPE_var | InterpreterObject:
|
|
varname, fallback = args
|
|
if isinstance(varname, Disabler):
|
|
return varname
|
|
|
|
try:
|
|
return self.variables[varname]
|
|
except KeyError:
|
|
if fallback is not None:
|
|
return self._holderify(fallback)
|
|
ustr = f'Tried to get unknown variable "{varname}".'
|
|
from difflib import get_close_matches
|
|
close_matches = get_close_matches(varname, self.variables.keys())
|
|
if close_matches:
|
|
ustr += f' Did you mean "{close_matches[0]}"?'
|
|
raise InterpreterException(ustr)
|
|
|
|
@TypedArgs('is_variable', pos_types=[STR_PARG])
|
|
def func_is_variable(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
|
|
return args[0] in self.variables
|
|
|
|
@FeatureNew('unset_variable', '0.60.0')
|
|
@TypedArgs('unset_variable', pos_types=[STR_PARG])
|
|
def func_unset_variable(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> None:
|
|
varname = args[0]
|
|
try:
|
|
del self.variables[varname]
|
|
except KeyError:
|
|
ustr = f'Tried to unset unknown variable "{varname}".'
|
|
from difflib import get_close_matches
|
|
close_matches = get_close_matches(varname, self.variables.keys())
|
|
if close_matches:
|
|
ustr += f' Did you mean "{close_matches[0]}"?'
|
|
raise InterpreterException(ustr)
|
|
|
|
@FeatureNew('is_disabler', '0.52.0')
|
|
@TypedArgs('is_disabler', pos_types=[OBJ_PARG])
|
|
def func_is_disabler(self, node: mparser.BaseNode, args: T.Tuple[object], kwargs: 'TYPE_kwargs') -> bool:
|
|
return isinstance(args[0], Disabler)
|
|
|
|
@FeatureNew('range', '0.58.0')
|
|
@TypedArgs(
|
|
'range',
|
|
pos_types=[INT_PARG],
|
|
opt_types=[INT_OARG, INT_OARG],
|
|
)
|
|
def func_range(self, node: mparser.BaseNode, args: T.Tuple[int, T.Optional[int], T.Optional[int]],
|
|
kwargs: TYPE_kwargs) -> P_OBJ.RangeHolder:
|
|
start, stop, step = args
|
|
# Just like Python's range, we allow range(stop), range(start, stop), or
|
|
# range(start, stop, step)
|
|
if stop is None:
|
|
stop = start
|
|
start = 0
|
|
if step is None:
|
|
step = 1
|
|
# This is more strict than Python's range()
|
|
if start < 0:
|
|
raise InterpreterException('start cannot be negative')
|
|
if stop < start:
|
|
raise InterpreterException('stop cannot be less than start')
|
|
if step < 1:
|
|
raise InterpreterException('step must be >=1')
|
|
return P_OBJ.RangeHolder(start, stop, step, subproject=self.subproject)
|