Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the Environment module fully type safe #14247

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions mesonbuild/compilers/compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,8 @@ def get_global_options(lang: str,

comp_options = env.options.get(comp_key, [])
link_options = env.options.get(largkey, [])
assert isinstance(comp_options, (str, list)), 'for mypy'
assert isinstance(link_options, (str, list)), 'for mypy'

cargs = options.UserStringArrayOption(
f'{lang}_{argkey.name}',
Expand Down
11 changes: 8 additions & 3 deletions mesonbuild/envconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from . import mlog
from pathlib import Path

if T.TYPE_CHECKING:
from .options import ElementaryOptionValues


# These classes contains all the data pulled from configuration files (native
# and cross file currently), and also assists with the reading environment
Expand Down Expand Up @@ -153,7 +156,7 @@ class CMakeSkipCompilerTest(Enum):
class Properties:
def __init__(
self,
properties: T.Optional[T.Dict[str, T.Optional[T.Union[str, bool, int, T.List[str]]]]] = None,
properties: T.Optional[T.Dict[str, ElementaryOptionValues]] = None,
):
self.properties = properties or {}

Expand Down Expand Up @@ -270,7 +273,9 @@ def __repr__(self) -> str:
return f'<MachineInfo: {self.system} {self.cpu_family} ({self.cpu})>'

@classmethod
def from_literal(cls, literal: T.Dict[str, str]) -> 'MachineInfo':
def from_literal(cls, raw: T.Dict[str, ElementaryOptionValues]) -> 'MachineInfo':
assert all(isinstance(v, str) for v in raw.values()), 'for mypy'
literal = T.cast('T.Dict[str, str]', raw)
minimum_literal = {'cpu', 'cpu_family', 'endian', 'system'}
if set(literal) < minimum_literal:
raise EnvironmentException(
Expand Down Expand Up @@ -389,7 +394,7 @@ class BinaryTable:

def __init__(
self,
binaries: T.Optional[T.Dict[str, T.Union[str, T.List[str]]]] = None,
binaries: T.Optional[T.Mapping[str, ElementaryOptionValues]] = None,
):
self.binaries: T.Dict[str, T.List[str]] = {}
if binaries:
Expand Down
69 changes: 44 additions & 25 deletions mesonbuild/environment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2012-2020 The Meson development team
# Copyright © 2023 Intel Corporation
# Copyright © 2023-2025 Intel Corporation

from __future__ import annotations

Expand Down Expand Up @@ -41,9 +41,9 @@
from mesonbuild import envconfig

if T.TYPE_CHECKING:
from configparser import ConfigParser

from .compilers import Compiler
from .compilers.mixins.visualstudio import VisualStudioLikeCompiler
from .options import ElementaryOptionValues
from .wrap.wrap import Resolver
from . import cargo

Expand All @@ -53,6 +53,11 @@
build_filename = 'meson.build'


def _as_str(val: object) -> str:
assert isinstance(val, str), 'for mypy'
return val


def _get_env_var(for_machine: MachineChoice, is_cross: bool, var_name: str) -> T.Optional[str]:
"""
Returns the exact env var and the value.
Expand All @@ -78,7 +83,8 @@ def _get_env_var(for_machine: MachineChoice, is_cross: bool, var_name: str) -> T
return value


def detect_gcovr(gcovr_exe: str = 'gcovr', min_version: str = '3.3', log: bool = False):
def detect_gcovr(gcovr_exe: str = 'gcovr', min_version: str = '3.3', log: bool = False) \
-> T.Union[T.Tuple[None, None], T.Tuple[str, str]]:
try:
p, found = Popen_safe([gcovr_exe, '--version'])[0:2]
except (FileNotFoundError, PermissionError):
Expand All @@ -91,7 +97,8 @@ def detect_gcovr(gcovr_exe: str = 'gcovr', min_version: str = '3.3', log: bool =
return gcovr_exe, found
return None, None

def detect_lcov(lcov_exe: str = 'lcov', log: bool = False):
def detect_lcov(lcov_exe: str = 'lcov', log: bool = False) \
-> T.Union[T.Tuple[None, None], T.Tuple[str, str]]:
try:
p, found = Popen_safe([lcov_exe, '--version'])[0:2]
except (FileNotFoundError, PermissionError):
Expand All @@ -104,7 +111,7 @@ def detect_lcov(lcov_exe: str = 'lcov', log: bool = False):
return lcov_exe, found
return None, None

def detect_llvm_cov(suffix: T.Optional[str] = None):
def detect_llvm_cov(suffix: T.Optional[str] = None) -> T.Optional[str]:
# If there's a known suffix or forced lack of suffix, use that
if suffix is not None:
if suffix == '':
Expand All @@ -121,7 +128,7 @@ def detect_llvm_cov(suffix: T.Optional[str] = None):
return tool
return None

def compute_llvm_suffix(coredata: coredata.CoreData):
def compute_llvm_suffix(coredata: coredata.CoreData) -> T.Optional[str]:
# Check to see if the user is trying to do coverage for either a C or C++ project
compilers = coredata.compilers[MachineChoice.BUILD]
cpp_compiler_is_clang = 'cpp' in compilers and compilers['cpp'].id == 'clang'
Expand All @@ -139,7 +146,8 @@ def compute_llvm_suffix(coredata: coredata.CoreData):
# Neither compiler is a Clang, or no compilers are for C or C++
return None

def detect_lcov_genhtml(lcov_exe: str = 'lcov', genhtml_exe: str = 'genhtml'):
def detect_lcov_genhtml(lcov_exe: str = 'lcov', genhtml_exe: str = 'genhtml') \
-> T.Tuple[str, T.Optional[str], str]:
lcov_exe, lcov_version = detect_lcov(lcov_exe)
if shutil.which(genhtml_exe) is None:
genhtml_exe = None
Expand All @@ -162,7 +170,7 @@ def detect_ninja(version: str = '1.8.2', log: bool = False) -> T.Optional[T.List
r = detect_ninja_command_and_version(version, log)
return r[0] if r else None

def detect_ninja_command_and_version(version: str = '1.8.2', log: bool = False) -> T.Tuple[T.List[str], str]:
def detect_ninja_command_and_version(version: str = '1.8.2', log: bool = False) -> T.Optional[T.Tuple[T.List[str], str]]:
env_ninja = os.environ.get('NINJA', None)
for n in [env_ninja] if env_ninja else ['ninja', 'ninja-build', 'samu']:
prog = ExternalProgram(n, silent=True)
Expand All @@ -188,6 +196,7 @@ def detect_ninja_command_and_version(version: str = '1.8.2', log: bool = False)
mlog.log('Found {}-{} at {}'.format(name, found,
' '.join([quote_arg(x) for x in prog.command])))
return (prog.command, found)
return None

def get_llvm_tool_names(tool: str) -> T.List[str]:
# Ordered list of possible suffixes of LLVM executables to try. Start with
Expand Down Expand Up @@ -334,6 +343,7 @@ def detect_windows_arch(compilers: CompilersDict) -> str:
# 32-bit and pretend like we're running under WOW64. Else, return the
# actual Windows architecture that we deduced above.
for compiler in compilers.values():
compiler = T.cast('VisualStudioLikeCompiler', compiler)
if compiler.id == 'msvc' and (compiler.target in {'x86', '80x86'}):
return 'x86'
if compiler.id == 'clang-cl' and (compiler.target in {'x86', 'i686'}):
Expand Down Expand Up @@ -532,7 +542,7 @@ def detect_machine_info(compilers: T.Optional[CompilersDict] = None) -> MachineI

# TODO make this compare two `MachineInfo`s purely. How important is the
# `detect_cpu_family({})` distinction? It is the one impediment to that.
def machine_info_can_run(machine_info: MachineInfo):
def machine_info_can_run(machine_info: MachineInfo) -> bool:
"""Whether we can run binaries for this machine on the current machine.

Can almost always run 32-bit binaries on 64-bit natively if the host
Expand Down Expand Up @@ -622,7 +632,7 @@ def __init__(self, source_dir: str, build_dir: str, cmd_options: coredata.Shared
#
# Note that order matters because of 'buildtype', if it is after
# 'optimization' and 'debug' keys, it override them.
self.options: T.MutableMapping[OptionKey, T.Union[str, T.List[str]]] = collections.OrderedDict()
self.options: T.MutableMapping[OptionKey, ElementaryOptionValues] = collections.OrderedDict()

## Read in native file(s) to override build machine configuration

Expand Down Expand Up @@ -691,7 +701,8 @@ def __init__(self, source_dir: str, build_dir: str, cmd_options: coredata.Shared
# Store a global state of Cargo dependencies
self.cargo: T.Optional[cargo.Interpreter] = None

def _load_machine_file_options(self, config: 'ConfigParser', properties: Properties, machine: MachineChoice) -> None:
def _load_machine_file_options(self, config: T.Mapping[str, T.Mapping[str, ElementaryOptionValues]],
properties: Properties, machine: MachineChoice) -> None:
"""Read the contents of a Machine file and put it in the options store."""

# Look for any options in the deprecated paths section, warn about
Expand All @@ -701,6 +712,7 @@ def _load_machine_file_options(self, config: 'ConfigParser', properties: Propert
if paths:
mlog.deprecation('The [paths] section is deprecated, use the [built-in options] section instead.')
for k, v in paths.items():
assert isinstance(v, (str, list)), 'for mypy'
self.options[OptionKey.from_string(k).evolve(machine=machine)] = v

# Next look for compiler options in the "properties" section, this is
Expand All @@ -713,6 +725,7 @@ def _load_machine_file_options(self, config: 'ConfigParser', properties: Propert
for k, v in properties.properties.copy().items():
if k in deprecated_properties:
mlog.deprecation(f'{k} in the [properties] section of the machine file is deprecated, use the [built-in options] section.')
assert isinstance(v, (str, list)), 'for mypy'
self.options[OptionKey.from_string(k).evolve(machine=machine)] = v
del properties.properties[k]

Expand Down Expand Up @@ -855,7 +868,12 @@ def create_new_coredata(self, options: coredata.SharedCMDOptions) -> None:
# re-initialized with project options by the interpreter during
# build file parsing.
# meson_command is used by the regenchecker script, which runs meson
self.coredata = coredata.CoreData(options, self.scratch_dir, mesonlib.get_meson_command())
meson_command = mesonlib.get_meson_command()
if meson_command is None:
meson_command = []
else:
meson_command = meson_command.copy()
self.coredata = coredata.CoreData(options, self.scratch_dir, meson_command)
self.first_invocation = True

def is_cross_build(self, when_building_for: MachineChoice = MachineChoice.HOST) -> bool:
Expand Down Expand Up @@ -896,7 +914,7 @@ def is_object(self, fname: 'mesonlib.FileOrString') -> bool:
return is_object(fname)

@lru_cache(maxsize=None)
def is_library(self, fname: mesonlib.FileOrString):
def is_library(self, fname: mesonlib.FileOrString) -> bool:
return is_library(fname)

def lookup_binary_entry(self, for_machine: MachineChoice, name: str) -> T.Optional[T.List[str]]:
Expand Down Expand Up @@ -936,25 +954,25 @@ def get_static_lib_dir(self) -> str:
return self.get_libdir()

def get_prefix(self) -> str:
return self.coredata.get_option(OptionKey('prefix'))
return _as_str(self.coredata.get_option(OptionKey('prefix')))

def get_libdir(self) -> str:
return self.coredata.get_option(OptionKey('libdir'))
return _as_str(self.coredata.get_option(OptionKey('libdir')))

def get_libexecdir(self) -> str:
return self.coredata.get_option(OptionKey('libexecdir'))
return _as_str(self.coredata.get_option(OptionKey('libexecdir')))

def get_bindir(self) -> str:
return self.coredata.get_option(OptionKey('bindir'))
return _as_str(self.coredata.get_option(OptionKey('bindir')))

def get_includedir(self) -> str:
return self.coredata.get_option(OptionKey('includedir'))
return _as_str(self.coredata.get_option(OptionKey('includedir')))

def get_mandir(self) -> str:
return self.coredata.get_option(OptionKey('mandir'))
return _as_str(self.coredata.get_option(OptionKey('mandir')))

def get_datadir(self) -> str:
return self.coredata.get_option(OptionKey('datadir'))
return _as_str(self.coredata.get_option(OptionKey('datadir')))

def get_compiler_system_lib_dirs(self, for_machine: MachineChoice) -> T.List[str]:
for comp in self.coredata.compilers[for_machine].values():
Expand All @@ -972,8 +990,8 @@ def get_compiler_system_lib_dirs(self, for_machine: MachineChoice) -> T.List[str
p, out, _ = Popen_safe(comp.get_exelist() + ['-print-search-dirs'])
if p.returncode != 0:
raise mesonlib.MesonException('Could not calculate system search dirs')
out = out.split('\n')[index].lstrip('libraries: =').split(':')
return [os.path.normpath(p) for p in out]
split = out.split('\n')[index].lstrip('libraries: =').split(':')
return [os.path.normpath(p) for p in split]

def get_compiler_system_include_dirs(self, for_machine: MachineChoice) -> T.List[str]:
for comp in self.coredata.compilers[for_machine].values():
Expand All @@ -987,9 +1005,10 @@ def get_compiler_system_include_dirs(self, for_machine: MachineChoice) -> T.List
return []
return comp.get_default_include_dirs()

def need_exe_wrapper(self, for_machine: MachineChoice = MachineChoice.HOST):
def need_exe_wrapper(self, for_machine: MachineChoice = MachineChoice.HOST) -> bool:
value = self.properties[for_machine].get('needs_exe_wrapper', None)
if value is not None:
assert isinstance(value, bool), 'for mypy'
return value
if not self.is_cross_build():
return False
Expand All @@ -1001,7 +1020,7 @@ def get_exe_wrapper(self) -> T.Optional[ExternalProgram]:
return self.exe_wrapper

def has_exe_wrapper(self) -> bool:
return self.exe_wrapper and self.exe_wrapper.found()
return self.exe_wrapper is not None and self.exe_wrapper.found()

def get_env_for_paths(self, library_paths: T.Set[str], extra_paths: T.Set[str]) -> mesonlib.EnvironmentVariables:
env = mesonlib.EnvironmentVariables()
Expand Down
22 changes: 10 additions & 12 deletions mesonbuild/machinefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,8 @@
from .mesonlib import MesonException

if T.TYPE_CHECKING:
from typing_extensions import TypeAlias

from .coredata import StrOrBytesPath

SectionT: TypeAlias = T.Union[str, int, bool, T.List[str], T.List['SectionT']]

from .options import ElementaryOptionValues

class CmdLineFileParser(configparser.ConfigParser):
def __init__(self) -> None:
Expand All @@ -36,8 +32,8 @@ def optionxform(self, optionstr: str) -> str:
class MachineFileParser():
def __init__(self, filenames: T.List[str], sourcedir: str) -> None:
self.parser = CmdLineFileParser()
self.constants: T.Dict[str, SectionT] = {'True': True, 'False': False}
self.sections: T.Dict[str, T.Dict[str, SectionT]] = {}
self.constants: T.Dict[str, ElementaryOptionValues] = {'True': True, 'False': False}
self.sections: T.Dict[str, T.Dict[str, ElementaryOptionValues]] = {}

for fname in filenames:
try:
Expand All @@ -62,9 +58,9 @@ def __init__(self, filenames: T.List[str], sourcedir: str) -> None:
continue
self.sections[s] = self._parse_section(s)

def _parse_section(self, s: str) -> T.Dict[str, SectionT]:
def _parse_section(self, s: str) -> T.Dict[str, ElementaryOptionValues]:
self.scope = self.constants.copy()
section: T.Dict[str, SectionT] = {}
section: T.Dict[str, ElementaryOptionValues] = {}
for entry, value in self.parser.items(s):
if ' ' in entry or '\t' in entry or "'" in entry or '"' in entry:
raise MesonException(f'Malformed variable name {entry!r} in machine file.')
Expand All @@ -83,7 +79,7 @@ def _parse_section(self, s: str) -> T.Dict[str, SectionT]:
self.scope[entry] = res
return section

def _evaluate_statement(self, node: mparser.BaseNode) -> SectionT:
def _evaluate_statement(self, node: mparser.BaseNode) -> ElementaryOptionValues:
if isinstance(node, (mparser.StringNode)):
return node.value
elif isinstance(node, mparser.BooleanNode):
Expand All @@ -93,7 +89,9 @@ def _evaluate_statement(self, node: mparser.BaseNode) -> SectionT:
elif isinstance(node, mparser.ParenthesizedNode):
return self._evaluate_statement(node.inner)
elif isinstance(node, mparser.ArrayNode):
return [self._evaluate_statement(arg) for arg in node.args.arguments]
a = [self._evaluate_statement(arg) for arg in node.args.arguments]
assert all(isinstance(s, str) for s in a), 'for mypy'
return T.cast('T.List[str]', a)
elif isinstance(node, mparser.IdNode):
return self.scope[node.value]
elif isinstance(node, mparser.ArithmeticNode):
Expand All @@ -109,7 +107,7 @@ def _evaluate_statement(self, node: mparser.BaseNode) -> SectionT:
return os.path.join(l, r)
raise MesonException('Unsupported node type')

def parse_machine_files(filenames: T.List[str], sourcedir: str) -> T.Dict[str, T.Dict[str, SectionT]]:
def parse_machine_files(filenames: T.List[str], sourcedir: str) -> T.Dict[str, T.Dict[str, ElementaryOptionValues]]:
parser = MachineFileParser(filenames, sourcedir)
return parser.sections

Expand Down
1 change: 1 addition & 0 deletions run_mypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
# 'mesonbuild/coredata.py',
'mesonbuild/depfile.py',
'mesonbuild/envconfig.py',
'mesonbuild/environment.py',
'mesonbuild/interpreter/compiler.py',
'mesonbuild/interpreter/mesonmain.py',
'mesonbuild/interpreter/interpreterobjects.py',
Expand Down
Loading