# Copyright 2012-2016 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import configparser, os, platform, re, sys, shlex, shutil, subprocess import typing from . import coredata from .linkers import ArLinker, ArmarLinker, VisualStudioLinker, DLinker, CcrxLinker from . import mesonlib from .mesonlib import ( MesonException, EnvironmentException, MachineChoice, PerMachine, Popen_safe ) from . import mlog from . import compilers from .compilers import ( CompilerType, is_assembly, is_header, is_library, is_llvm_ir, is_object, is_source, ) from .compilers import ( Compiler, ArmCCompiler, ArmCPPCompiler, ArmclangCCompiler, ArmclangCPPCompiler, ClangCCompiler, ClangCPPCompiler, ClangObjCCompiler, ClangObjCPPCompiler, ClangClCCompiler, ClangClCPPCompiler, FlangFortranCompiler, G95FortranCompiler, GnuCCompiler, GnuCPPCompiler, GnuFortranCompiler, GnuObjCCompiler, GnuObjCPPCompiler, ElbrusCCompiler, ElbrusCPPCompiler, ElbrusFortranCompiler, IntelCCompiler, IntelCPPCompiler, IntelFortranCompiler, JavaCompiler, MonoCompiler, CudaCompiler, VisualStudioCsCompiler, NAGFortranCompiler, Open64FortranCompiler, PathScaleFortranCompiler, PGICCompiler, PGICPPCompiler, PGIFortranCompiler, RustCompiler, CcrxCCompiler, CcrxCPPCompiler, SunFortranCompiler, ValaCompiler, VisualStudioCCompiler, VisualStudioCPPCompiler, ) build_filename = 'meson.build' known_cpu_families = ( 'aarch64', 'arc', 'arm', 'e2k', 'ia64', 'mips', 'mips64', 'parisc', 'ppc', 'ppc64', 'riscv32', 'riscv64', 'rl78', 'rx', 's390x', 'sparc', 'sparc64', 'x86', 'x86_64' ) # Environment variables that each lang uses. cflags_mapping = {'c': 'CFLAGS', 'cpp': 'CXXFLAGS', 'cu': 'CUFLAGS', 'objc': 'OBJCFLAGS', 'objcpp': 'OBJCXXFLAGS', 'fortran': 'FFLAGS', 'd': 'DFLAGS', 'vala': 'VALAFLAGS'} def detect_gcovr(version='3.1', log=False): gcovr_exe = 'gcovr' try: p, found = Popen_safe([gcovr_exe, '--version'])[0:2] except (FileNotFoundError, PermissionError): # Doesn't exist in PATH or isn't executable return None, None found = search_version(found) if p.returncode == 0: if log: mlog.log('Found gcovr-{} at {}'.format(found, shlex.quote(shutil.which(gcovr_exe)))) return gcovr_exe, mesonlib.version_compare(found, '>=' + version) return None, None def find_coverage_tools(): gcovr_exe, gcovr_new_rootdir = detect_gcovr() lcov_exe = 'lcov' genhtml_exe = 'genhtml' if not mesonlib.exe_exists([lcov_exe, '--version']): lcov_exe = None if not mesonlib.exe_exists([genhtml_exe, '--version']): genhtml_exe = None return gcovr_exe, gcovr_new_rootdir, lcov_exe, genhtml_exe def detect_ninja(version='1.5', log=False): for n in ['ninja', 'ninja-build', 'samu']: try: p, found = Popen_safe([n, '--version'])[0:2] except (FileNotFoundError, PermissionError): # Doesn't exist in PATH or isn't executable continue found = found.strip() # Perhaps we should add a way for the caller to know the failure mode # (not found or too old) if p.returncode == 0 and mesonlib.version_compare(found, '>=' + version): if log: mlog.log('Found ninja-{} at {}'.format(found, shlex.quote(shutil.which(n)))) return n def detect_native_windows_arch(): """ The architecture of Windows itself: x86, amd64 or arm64 """ # These env variables are always available. See: # https://msdn.microsoft.com/en-us/library/aa384274(VS.85).aspx # https://blogs.msdn.microsoft.com/david.wang/2006/03/27/howto-detect-process-bitness/ arch = os.environ.get('PROCESSOR_ARCHITEW6432', '').lower() if not arch: try: # If this doesn't exist, something is messing with the environment arch = os.environ['PROCESSOR_ARCHITECTURE'].lower() except KeyError: raise EnvironmentException('Unable to detect native OS architecture') return arch def detect_windows_arch(compilers): """ Detecting the 'native' architecture of Windows is not a trivial task. We cannot trust that the architecture that Python is built for is the 'native' one because you can run 32-bit apps on 64-bit Windows using WOW64 and people sometimes install 32-bit Python on 64-bit Windows. We also can't rely on the architecture of the OS itself, since it's perfectly normal to compile and run 32-bit applications on Windows as if they were native applications. It's a terrible experience to require the user to supply a cross-info file to compile 32-bit applications on 64-bit Windows. Thankfully, the only way to compile things with Visual Studio on Windows is by entering the 'msvc toolchain' environment, which can be easily detected. In the end, the sanest method is as follows: 1. Check environment variables that are set by Windows and WOW64 to find out if this is x86 (possibly in WOW64), if so use that as our 'native' architecture. 2. If the compiler toolchain target architecture is x86, use that as our 'native' architecture. 3. Otherwise, use the actual Windows architecture """ os_arch = detect_native_windows_arch() if os_arch == 'x86': return os_arch # If we're on 64-bit Windows, 32-bit apps can be compiled without # cross-compilation. So if we're doing that, just set the native arch as # 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(): if compiler.id == 'msvc' and compiler.target == 'x86': return 'x86' if compiler.id == 'clang-cl' and compiler.target == 'x86': return 'x86' if compiler.id == 'gcc' and compiler.has_builtin_define('__i386__'): return 'x86' return os_arch def any_compiler_has_define(compilers, define): for c in compilers.values(): try: if c.has_builtin_define(define): return True except mesonlib.MesonException: # Ignore compilers that do not support has_builtin_define. pass return False def detect_cpu_family(compilers): """ Python is inconsistent in its platform module. It returns different values for the same cpu. For x86 it might return 'x86', 'i686' or somesuch. Do some canonicalization. """ if mesonlib.is_windows(): trial = detect_windows_arch(compilers) else: trial = platform.machine().lower() if trial.startswith('i') and trial.endswith('86'): trial = 'x86' elif trial.startswith('arm'): trial = 'arm' elif trial.startswith('ppc64'): trial = 'ppc64' elif trial == 'macppc': trial = 'ppc' elif trial == 'powerpc': trial = 'ppc' # FreeBSD calls both ppc and ppc64 "powerpc". # https://github.com/mesonbuild/meson/issues/4397 try: p, stdo, _ = Popen_safe(['uname', '-p']) except (FileNotFoundError, PermissionError): # Not much to go on here. if sys.maxsize > 2**32: trial = 'ppc64' if 'powerpc64' in stdo: trial = 'ppc64' elif trial in ('amd64', 'x64'): trial = 'x86_64' # On Linux (and maybe others) there can be any mixture of 32/64 bit code in # the kernel, Python, system, 32-bit chroot on 64-bit host, etc. The only # reliable way to know is to check the compiler defines. if trial == 'x86_64': if any_compiler_has_define(compilers, '__i386__'): trial = 'x86' elif trial == 'aarch64': if any_compiler_has_define(compilers, '__arm__'): trial = 'arm' # Add more quirks here as bugs are reported. Keep in sync with detect_cpu() # below. elif trial == 'parisc64': # ATM there is no 64 bit userland for PA-RISC. Thus always # report it as 32 bit for simplicity. trial = 'parisc' if trial not in known_cpu_families: mlog.warning('Unknown CPU family {!r}, please report this at ' 'https://github.com/mesonbuild/meson/issues/new with the' 'output of `uname -a` and `cat /proc/cpuinfo`'.format(trial)) return trial def detect_cpu(compilers): if mesonlib.is_windows(): trial = detect_windows_arch(compilers) else: trial = platform.machine().lower() if trial in ('amd64', 'x64'): trial = 'x86_64' if trial == 'x86_64': # Same check as above for cpu_family if any_compiler_has_define(compilers, '__i386__'): trial = 'i686' # All 64 bit cpus have at least this level of x86 support. elif trial == 'aarch64': # Same check as above for cpu_family if any_compiler_has_define(compilers, '__arm__'): trial = 'arm' elif trial == 'e2k': # Make more precise CPU detection for Elbrus platform. trial = platform.processor().lower() # Add more quirks here as bugs are reported. Keep in sync with # detect_cpu_family() above. return trial def detect_system(): system = platform.system().lower() if system.startswith('cygwin'): return 'cygwin' return system def detect_msys2_arch(): if 'MSYSTEM_CARCH' in os.environ: return os.environ['MSYSTEM_CARCH'] return None def search_version(text): # Usually of the type 4.1.4 but compiler output may contain # stuff like this: # (Sourcery CodeBench Lite 2014.05-29) 4.8.3 20140320 (prerelease) # Limiting major version number to two digits seems to work # thus far. When we get to GCC 100, this will break, but # if we are still relevant when that happens, it can be # considered an achievement in itself. # # This regex is reaching magic levels. If it ever needs # to be updated, do not complexify but convert to something # saner instead. version_regex = r'(? str: "Install dir for the import library (library used for linking)" return self.get_libdir() def get_shared_module_dir(self) -> str: "Install dir for shared modules that are loaded at runtime" return self.get_libdir() def get_shared_lib_dir(self) -> str: "Install dir for the shared library" if self.win_libdir_layout: return self.get_bindir() return self.get_libdir() def get_static_lib_dir(self) -> str: "Install dir for the static library" return self.get_libdir() def get_object_suffix(self): return self.object_suffix def get_prefix(self) -> str: return self.coredata.get_builtin_option('prefix') def get_libdir(self) -> str: return self.coredata.get_builtin_option('libdir') def get_libexecdir(self) -> str: return self.coredata.get_builtin_option('libexecdir') def get_bindir(self) -> str: return self.coredata.get_builtin_option('bindir') def get_includedir(self) -> str: return self.coredata.get_builtin_option('includedir') def get_mandir(self) -> str: return self.coredata.get_builtin_option('mandir') def get_datadir(self) -> str: return self.coredata.get_builtin_option('datadir') def get_compiler_system_dirs(self): for comp in self.coredata.compilers.values(): if isinstance(comp, compilers.ClangCompiler): index = 1 break elif isinstance(comp, compilers.GnuCompiler): index = 2 break else: # This option is only supported by gcc and clang. If we don't get a # GCC or Clang compiler return and empty list. return [] 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] def need_exe_wrapper(self, for_machine: MachineChoice = MachineChoice.HOST): value = self.properties[for_machine].get('needs_exe_wrapper', None) if value is not None: return value return not self.machines[for_machine].can_run() def get_exe_wrapper(self): if not self.need_exe_wrapper(): from .dependencies import EmptyExternalProgram return EmptyExternalProgram() return self.exe_wrapper class MesonConfigFile: @classmethod def parse_datafile(cls, filename): config = configparser.ConfigParser() try: with open(filename, 'r') as f: config.read_file(f, filename) except FileNotFoundError: raise EnvironmentException('File not found: %s.' % filename) return cls.from_config_parser(config) @classmethod def from_config_parser(cls, parser: configparser.ConfigParser): out = {} # This is a bit hackish at the moment. for s in parser.sections(): section = {} for entry in parser[s]: value = parser[s][entry] # Windows paths... value = value.replace('\\', '\\\\') if ' ' in entry or '\t' in entry or "'" in entry or '"' in entry: raise EnvironmentException('Malformed variable name %s in cross file..' % entry) try: res = eval(value, {'__builtins__': None}, {'true': True, 'false': False}) except Exception: raise EnvironmentException('Malformed value in cross file variable %s.' % entry) for i in (res if isinstance(res, list) else [res]): if not isinstance(i, (str, int, bool)): raise EnvironmentException('Malformed value in cross file variable %s.' % entry) section[entry] = res out[s] = section return out class Properties: def __init__( self, properties: typing.Optional[typing.Dict[str, typing.Union[str, typing.List[str]]]] = None): self.properties = properties or {} def has_stdlib(self, language): return language + '_stdlib' in self.properties def get_stdlib(self, language): return self.properties[language + '_stdlib'] def get_root(self): return self.properties.get('root', None) def get_sys_root(self): return self.properties.get('sys_root', None) def __eq__(self, other): if isinstance(other, type(self)): return self.properties == other.properties return NotImplemented # TODO consider removing so Properties is less freeform def __getitem__(self, key): return self.properties[key] # TODO consider removing so Properties is less freeform def __contains__(self, item): return item in self.properties # TODO consider removing, for same reasons as above def get(self, key, default=None): return self.properties.get(key, default) class MachineInfo: def __init__(self, system, cpu_family, cpu, endian): self.system = system self.cpu_family = cpu_family self.cpu = cpu self.endian = endian def __eq__(self, other): if self.__class__ is not other.__class__: return NotImplemented return \ self.system == other.system and \ self.cpu_family == other.cpu_family and \ self.cpu == other.cpu and \ self.endian == other.endian def __ne__(self, other): if self.__class__ is not other.__class__: return NotImplemented return not self.__eq__(other) def __repr__(self): return ''.format(self.system, self.cpu_family, self.cpu) @staticmethod def detect(compilers = None): """Detect the machine we're running on If compilers are not provided, we cannot know as much. None out those fields to avoid accidentally depending on partial knowledge. The underlying ''detect_*'' method can be called to explicitly use the partial information. """ return MachineInfo( detect_system(), detect_cpu_family(compilers) if compilers is not None else None, detect_cpu(compilers) if compilers is not None else None, sys.byteorder) @staticmethod def from_literal(literal): minimum_literal = {'cpu', 'cpu_family', 'endian', 'system'} if set(literal) < minimum_literal: raise EnvironmentException( 'Machine info is currently {}\n'.format(literal) + 'but is missing {}.'.format(minimum_literal - set(literal))) cpu_family = literal['cpu_family'] if cpu_family not in known_cpu_families: mlog.warning('Unknown CPU family %s, please report this at https://github.com/mesonbuild/meson/issues/new' % cpu_family) endian = literal['endian'] if endian not in ('little', 'big'): mlog.warning('Unknown endian %s' % endian) return MachineInfo( literal['system'], cpu_family, literal['cpu'], endian) def is_windows(self): """ Machine is windows? """ return self.system == 'windows' def is_cygwin(self): """ Machine is cygwin? """ return self.system == 'cygwin' def is_linux(self): """ Machine is linux? """ return self.system == 'linux' def is_darwin(self): """ Machine is Darwin (iOS/OS X)? """ return self.system in ('darwin', 'ios') def is_android(self): """ Machine is Android? """ return self.system == 'android' def is_haiku(self): """ Machine is Haiku? """ return self.system == 'haiku' def is_openbsd(self): """ Machine is OpenBSD? """ return self.system == 'openbsd' # Various prefixes and suffixes for import libraries, shared libraries, # static libraries, and executables. # Versioning is added to these names in the backends as-needed. def get_exe_suffix(self): if self.is_windows() or self.is_cygwin(): return 'exe' else: return '' def get_object_suffix(self): if self.is_windows(): return 'obj' else: return 'o' def libdir_layout_is_win(self): return self.is_windows() \ or self.is_cygwin() # TODO make this compare two `MachineInfo`s purely. How important is the # `detect_cpu_family({})` distinction? It is the one impediment to that. def can_run(self): """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 and build systems are the same. We don't pass any compilers to detect_cpu_family() here because we always want to know the OS architecture, not what the compiler environment tells us. """ if self.system != detect_system(): return False true_build_cpu_family = detect_cpu_family({}) return \ (self.cpu_family == true_build_cpu_family) or \ ((true_build_cpu_family == 'x86_64') and (self.cpu_family == 'x86')) class PerMachineDefaultable(PerMachine): """Extends `PerMachine` with the ability to default from `None`s. """ def __init__(self): super().__init__(None, None, None) def default_missing(self): """Default host to buid and target to host. This allows just specifying nothing in the native case, just host in the cross non-compiler case, and just target in the native-built cross-compiler case. """ if self.host is None: self.host = self.build if self.target is None: self.target = self.host def miss_defaulting(self): """Unset definition duplicated from their previous to None This is the inverse of ''default_missing''. By removing defaulted machines, we can elaborate the original and then redefault them and thus avoid repeating the elaboration explicitly. """ if self.target == self.host: self.target = None if self.host == self.build: self.host = None class MachineInfos(PerMachineDefaultable): def detect_build(self, compilers = None): self.build = MachineInfo.detect(compilers) def matches_build_machine(self, machine: MachineChoice): return self.build == self[machine] class BinaryTable: def __init__(self, binaries = {}, fallback = True): self.binaries = binaries self.fallback = fallback for name, command in self.binaries.items(): if not isinstance(command, (list, str)): # TODO generalize message raise mesonlib.MesonException( 'Invalid type {!r} for binary {!r} in cross file' ''.format(command, name)) # Map from language identifiers to environment variables. evarMap = { # Compilers 'c': 'CC', 'cpp': 'CXX', 'cs': 'CSC', 'd': 'DC', 'fortran': 'FC', 'objc': 'OBJC', 'objcpp': 'OBJCXX', 'rust': 'RUSTC', 'vala': 'VALAC', # Binutils 'strip': 'STRIP', 'ar': 'AR', 'windres': 'WINDRES', 'cmake': 'CMAKE', 'qmake': 'QMAKE', 'pkgconfig': 'PKG_CONFIG', } @classmethod def detect_ccache(cls): try: has_ccache = subprocess.call(['ccache', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError: has_ccache = 1 if has_ccache == 0: cmdlist = ['ccache'] else: cmdlist = [] return cmdlist @classmethod def _warn_about_lang_pointing_to_cross(cls, compiler_exe, evar): evar_str = os.environ.get(evar, 'WHO_WOULD_CALL_THEIR_COMPILER_WITH_THIS_NAME') if evar_str == compiler_exe: mlog.warning('''Env var %s seems to point to the cross compiler. This is probably wrong, it should always point to the native compiler.''' % evar) @classmethod def parse_entry(cls, entry): compiler = mesonlib.stringlistify(entry) # Ensure ccache exists and remove it if it doesn't if compiler[0] == 'ccache': compiler = compiler[1:] ccache = cls.detect_ccache() else: ccache = [] # Return value has to be a list of compiler 'choices' return compiler, ccache def lookup_entry(self, name): """Lookup binary Returns command with args as list if found, Returns `None` if nothing is found. First tries looking in explicit map, then tries environment variable. """ # Try explict map, don't fall back on env var command = self.binaries.get(name) if command is not None: command = mesonlib.stringlistify(command) # Relies on there being no "" env var evar = self.evarMap.get(name, "") self._warn_about_lang_pointing_to_cross(command[0], evar) elif self.fallback: # Relies on there being no "" env var evar = self.evarMap.get(name, "") command = os.environ.get(evar) if command is not None: command = shlex.split(command) return command class Directories: """Data class that holds information about directories for native and cross builds. """ def __init__(self, bindir: typing.Optional[str] = None, datadir: typing.Optional[str] = None, includedir: typing.Optional[str] = None, infodir: typing.Optional[str] = None, libdir: typing.Optional[str] = None, libexecdir: typing.Optional[str] = None, localedir: typing.Optional[str] = None, localstatedir: typing.Optional[str] = None, mandir: typing.Optional[str] = None, prefix: typing.Optional[str] = None, sbindir: typing.Optional[str] = None, sharedstatedir: typing.Optional[str] = None, sysconfdir: typing.Optional[str] = None): self.bindir = bindir self.datadir = datadir self.includedir = includedir self.infodir = infodir self.libdir = libdir self.libexecdir = libexecdir self.localedir = localedir self.localstatedir = localstatedir self.mandir = mandir self.prefix = prefix self.sbindir = sbindir self.sharedstatedir = sharedstatedir self.sysconfdir = sysconfdir def __contains__(self, key: str) -> str: return hasattr(self, key) def __getitem__(self, key: str) -> str: return getattr(self, key) def __setitem__(self, key: str, value: typing.Optional[str]) -> None: setattr(self, key, value) def __iter__(self) -> typing.Iterator[typing.Tuple[str, str]]: return iter(self.__dict__.items())