# SPDX-License-Identifier: Apache-2.0 # Copyright 2022 The Meson development team from __future__ import annotations from dataclasses import dataclass, field import sys, os, subprocess, shutil import shlex import typing as T from .. import envconfig from .. import mlog from ..compilers import compilers from ..compilers.detect import defaults as compiler_names if T.TYPE_CHECKING: import argparse def has_for_build() -> bool: for cenv in envconfig.ENV_VAR_COMPILER_MAP.values(): if os.environ.get(cenv + '_FOR_BUILD'): return True return False # Note: when adding arguments, please also add them to the completion # scripts in $MESONSRC/data/shell-completions/ def add_arguments(parser: 'argparse.ArgumentParser') -> None: parser.add_argument('--debarch', default=None, help='The dpkg architecture to generate.') parser.add_argument('--gccsuffix', default="", help='A particular gcc version suffix if necessary.') parser.add_argument('-o', required=True, dest='outfile', help='The output file.') parser.add_argument('--cross', default=False, action='store_true', help='Generate a cross compilation file.') parser.add_argument('--native', default=False, action='store_true', help='Generate a native compilation file.') parser.add_argument('--system', default=None, help='Define system for cross compilation.') parser.add_argument('--subsystem', default=None, help='Define subsystem for cross compilation.') parser.add_argument('--kernel', default=None, help='Define kernel for cross compilation.') parser.add_argument('--cpu', default=None, help='Define cpu for cross compilation.') parser.add_argument('--cpu-family', default=None, help='Define cpu family for cross compilation.') parser.add_argument('--endian', default='little', choices=['big', 'little'], help='Define endianness for cross compilation.') @dataclass class MachineInfo: compilers: T.Dict[str, T.List[str]] = field(default_factory=dict) binaries: T.Dict[str, T.List[str]] = field(default_factory=dict) properties: T.Dict[str, T.Union[str, T.List[str]]] = field(default_factory=dict) compile_args: T.Dict[str, T.List[str]] = field(default_factory=dict) link_args: T.Dict[str, T.List[str]] = field(default_factory=dict) cmake: T.Dict[str, T.Union[str, T.List[str]]] = field(default_factory=dict) system: T.Optional[str] = None subsystem: T.Optional[str] = None kernel: T.Optional[str] = None cpu: T.Optional[str] = None cpu_family: T.Optional[str] = None endian: T.Optional[str] = None #parser = argparse.ArgumentParser(description='''Generate cross compilation definition file for the Meson build system. # #If you do not specify the --arch argument, Meson assumes that running #plain 'dpkg-architecture' will return correct information for the #host system. # #This script must be run in an environment where CPPFLAGS et al are set to the #same values used in the actual compilation. #''' #) def locate_path(program: str) -> T.List[str]: if os.path.isabs(program): return [program] for d in os.get_exec_path(): f = os.path.join(d, program) if os.access(f, os.X_OK): return [f] raise ValueError("%s not found on $PATH" % program) def write_args_line(ofile: T.TextIO, name: str, args: T.Union[str, T.List[str]]) -> None: if len(args) == 0: return if isinstance(args, str): ostr = name + "= '" + args + "'\n" else: ostr = name + ' = [' ostr += ', '.join("'" + i + "'" for i in args) ostr += ']\n' ofile.write(ostr) def get_args_from_envvars(infos: MachineInfo) -> None: cppflags = shlex.split(os.environ.get('CPPFLAGS', '')) cflags = shlex.split(os.environ.get('CFLAGS', '')) cxxflags = shlex.split(os.environ.get('CXXFLAGS', '')) objcflags = shlex.split(os.environ.get('OBJCFLAGS', '')) objcxxflags = shlex.split(os.environ.get('OBJCXXFLAGS', '')) ldflags = shlex.split(os.environ.get('LDFLAGS', '')) c_args = cppflags + cflags cpp_args = cppflags + cxxflags c_link_args = cflags + ldflags cpp_link_args = cxxflags + ldflags objc_args = cppflags + objcflags objcpp_args = cppflags + objcxxflags objc_link_args = objcflags + ldflags objcpp_link_args = objcxxflags + ldflags if c_args: infos.compile_args['c'] = c_args if c_link_args: infos.link_args['c'] = c_link_args if cpp_args: infos.compile_args['cpp'] = cpp_args if cpp_link_args: infos.link_args['cpp'] = cpp_link_args if objc_args: infos.compile_args['objc'] = objc_args if objc_link_args: infos.link_args['objc'] = objc_link_args if objcpp_args: infos.compile_args['objcpp'] = objcpp_args if objcpp_link_args: infos.link_args['objcpp'] = objcpp_link_args # map from DEB_HOST_GNU_CPU to Meson machine.cpu_family() deb_cpu_family_map = { 'mips64el': 'mips64', 'i686': 'x86', 'powerpc64le': 'ppc64', } # map from DEB_HOST_ARCH to Meson machine.cpu() deb_arch_cpu_map = { 'armhf': 'arm7hlf', } # map from DEB_HOST_GNU_CPU to Meson machine.cpu() deb_cpu_map = { 'mips64el': 'mips64', 'powerpc64le': 'ppc64', } # map from DEB_HOST_ARCH_OS to Meson machine.system() deb_os_map = { 'hurd': 'gnu', } # map from DEB_HOST_ARCH_OS to Meson machine.kernel() deb_kernel_map = { 'kfreebsd': 'freebsd', 'hurd': 'gnu', } def replace_special_cases(special_cases: T.Mapping[str, str], name: str) -> str: ''' If name is a key in special_cases, replace it with the value, or otherwise pass it through unchanged. ''' return special_cases.get(name, name) def deb_detect_cmake(infos: MachineInfo, data: T.Dict[str, str]) -> None: system_name_map = {'linux': 'Linux', 'kfreebsd': 'kFreeBSD', 'hurd': 'GNU'} system_processor_map = {'arm': 'armv7l', 'mips64el': 'mips64', 'powerpc64le': 'ppc64le'} infos.cmake["CMAKE_C_COMPILER"] = infos.compilers['c'] try: infos.cmake["CMAKE_CXX_COMPILER"] = infos.compilers['cpp'] except KeyError: pass infos.cmake["CMAKE_SYSTEM_NAME"] = system_name_map[data['DEB_HOST_ARCH_OS']] infos.cmake["CMAKE_SYSTEM_PROCESSOR"] = replace_special_cases(system_processor_map, data['DEB_HOST_GNU_CPU']) def deb_compiler_lookup(infos: MachineInfo, compilerstems: T.List[T.Tuple[str, str]], host_arch: str, gccsuffix: str) -> None: for langname, stem in compilerstems: compilername = f'{host_arch}-{stem}{gccsuffix}' try: p = locate_path(compilername) infos.compilers[langname] = p except ValueError: pass def detect_cross_debianlike(options: T.Any) -> MachineInfo: if options.debarch == 'auto': cmd = ['dpkg-architecture'] else: cmd = ['dpkg-architecture', '-a' + options.debarch] output = subprocess.check_output(cmd, universal_newlines=True, stderr=subprocess.DEVNULL) return dpkg_architecture_to_machine_info(output, options) def dpkg_architecture_to_machine_info(output: str, options: T.Any) -> MachineInfo: data = {} for line in output.split('\n'): line = line.strip() if line == '': continue k, v = line.split('=', 1) data[k] = v host_arch = data['DEB_HOST_GNU_TYPE'] host_os = replace_special_cases(deb_os_map, data['DEB_HOST_ARCH_OS']) host_subsystem = host_os host_kernel = replace_special_cases(deb_kernel_map, data['DEB_HOST_ARCH_OS']) host_cpu_family = replace_special_cases(deb_cpu_family_map, data['DEB_HOST_GNU_CPU']) host_cpu = deb_arch_cpu_map.get(data['DEB_HOST_ARCH'], replace_special_cases(deb_cpu_map, data['DEB_HOST_GNU_CPU'])) host_endian = data['DEB_HOST_ARCH_ENDIAN'] compilerstems = [('c', 'gcc'), ('cpp', 'g++'), ('objc', 'gobjc'), ('objcpp', 'gobjc++')] infos = MachineInfo() deb_compiler_lookup(infos, compilerstems, host_arch, options.gccsuffix) if len(infos.compilers) == 0: print('Warning: no compilers were detected.') infos.binaries['ar'] = locate_path("%s-ar" % host_arch) infos.binaries['strip'] = locate_path("%s-strip" % host_arch) infos.binaries['objcopy'] = locate_path("%s-objcopy" % host_arch) infos.binaries['ld'] = locate_path("%s-ld" % host_arch) try: infos.binaries['cmake'] = locate_path("cmake") deb_detect_cmake(infos, data) except ValueError: pass for tool in [ 'g-ir-annotation-tool', 'g-ir-compiler', 'g-ir-doc-tool', 'g-ir-generate', 'g-ir-inspect', 'g-ir-scanner', 'pkg-config', ]: try: infos.binaries[tool] = locate_path("%s-%s" % (host_arch, tool)) except ValueError: pass # optional for tool, exe in [ ('exe_wrapper', 'cross-exe-wrapper'), ]: try: infos.binaries[tool] = locate_path("%s-%s" % (host_arch, exe)) except ValueError: pass for tool, exe in [ ('vala', 'valac'), ]: try: infos.compilers[tool] = locate_path("%s-%s" % (host_arch, exe)) except ValueError: pass try: infos.binaries['cups-config'] = locate_path("cups-config") except ValueError: pass infos.system = host_os infos.subsystem = host_subsystem infos.kernel = host_kernel infos.cpu_family = host_cpu_family infos.cpu = host_cpu infos.endian = host_endian get_args_from_envvars(infos) return infos def write_machine_file(infos: MachineInfo, ofilename: str, write_system_info: bool) -> None: tmpfilename = ofilename + '~' with open(tmpfilename, 'w', encoding='utf-8') as ofile: ofile.write('[binaries]\n') ofile.write('# Compilers\n') for langname in sorted(infos.compilers.keys()): compiler = infos.compilers[langname] write_args_line(ofile, langname, compiler) ofile.write('\n') ofile.write('# Other binaries\n') for exename in sorted(infos.binaries.keys()): exe = infos.binaries[exename] write_args_line(ofile, exename, exe) ofile.write('\n') ofile.write('[built-in options]\n') all_langs = list(set(infos.compile_args.keys()).union(set(infos.link_args.keys()))) all_langs.sort() for lang in all_langs: if lang in infos.compile_args: write_args_line(ofile, lang + '_args', infos.compile_args[lang]) if lang in infos.link_args: write_args_line(ofile, lang + '_link_args', infos.link_args[lang]) ofile.write('\n') ofile.write('[properties]\n') for k, v in infos.properties.items(): write_args_line(ofile, k, v) ofile.write('\n') if infos.cmake: ofile.write('[cmake]\n\n') for k, v in infos.cmake.items(): write_args_line(ofile, k, v) ofile.write('\n') if write_system_info: ofile.write('[host_machine]\n') ofile.write(f"cpu = '{infos.cpu}'\n") ofile.write(f"cpu_family = '{infos.cpu_family}'\n") ofile.write(f"endian = '{infos.endian}'\n") ofile.write(f"system = '{infos.system}'\n") if infos.subsystem: ofile.write(f"subsystem = '{infos.subsystem}'\n") if infos.kernel: ofile.write(f"kernel = '{infos.kernel}'\n") os.replace(tmpfilename, ofilename) def detect_language_args_from_envvars(langname: str, envvar_suffix: str = '') -> T.Tuple[T.List[str], T.List[str]]: compile_args = [] if langname in compilers.CFLAGS_MAPPING: compile_args = shlex.split(os.environ.get(compilers.CFLAGS_MAPPING[langname] + envvar_suffix, '')) if langname in compilers.LANGUAGES_USING_CPPFLAGS: cppflags = tuple(shlex.split(os.environ.get('CPPFLAGS' + envvar_suffix, ''))) lang_compile_args = list(cppflags) + compile_args else: lang_compile_args = compile_args lang_link_args = [] if langname in compilers.LANGUAGES_USING_LDFLAGS: lang_link_args += shlex.split(os.environ.get('LDFLAGS' + envvar_suffix, '')) lang_link_args += compile_args return (lang_compile_args, lang_link_args) def detect_compilers_from_envvars(envvar_suffix: str = '') -> MachineInfo: infos = MachineInfo() for langname, envvarname in envconfig.ENV_VAR_COMPILER_MAP.items(): compilerstr = os.environ.get(envvarname + envvar_suffix) if not compilerstr: continue if os.path.exists(compilerstr): compiler = [compilerstr] else: compiler = shlex.split(compilerstr) infos.compilers[langname] = compiler lang_compile_args, lang_link_args = detect_language_args_from_envvars(langname, envvar_suffix) if lang_compile_args: infos.compile_args[langname] = lang_compile_args if lang_link_args: infos.link_args[langname] = lang_link_args return infos def detect_binaries_from_envvars(infos: MachineInfo, envvar_suffix: str = '') -> None: for binname, envvar_base in envconfig.ENV_VAR_TOOL_MAP.items(): envvar = envvar_base + envvar_suffix binstr = os.environ.get(envvar) if binstr: infos.binaries[binname] = shlex.split(binstr) def detect_properties_from_envvars(infos: MachineInfo, envvar_suffix: str = '') -> None: var = os.environ.get('PKG_CONFIG_LIBDIR' + envvar_suffix) if var is not None: infos.properties['pkg_config_libdir'] = var var = os.environ.get('PKG_CONFIG_SYSROOT_DIR' + envvar_suffix) if var is not None: infos.properties['sys_root'] = var def detect_cross_system(infos: MachineInfo, options: T.Any) -> None: for optname in ('system', 'subsystem', 'kernel', 'cpu', 'cpu_family', 'endian'): v = getattr(options, optname) if not v: mlog.error(f'Cross property "{optname}" missing, set it with --{optname.replace("_", "-")}.') sys.exit(1) setattr(infos, optname, v) def detect_cross_env(options: T.Any) -> MachineInfo: if options.debarch: print('Detecting cross environment via dpkg-architecture.') infos = detect_cross_debianlike(options) else: print('Detecting cross environment via environment variables.') infos = detect_compilers_from_envvars() detect_cross_system(infos, options) detect_binaries_from_envvars(infos) detect_properties_from_envvars(infos) return infos def add_compiler_if_missing(infos: MachineInfo, langname: str, exe_names: T.List[str]) -> None: if langname in infos.compilers: return for exe_name in exe_names: lookup = shutil.which(exe_name) if not lookup: continue compflags, linkflags = detect_language_args_from_envvars(langname) infos.compilers[langname] = [lookup] if compflags: infos.compile_args[langname] = compflags if linkflags: infos.link_args[langname] = linkflags return def detect_missing_native_compilers(infos: MachineInfo) -> None: # T.Any per-platform special detection should go here. for langname, exes in compiler_names.items(): if langname not in envconfig.ENV_VAR_COMPILER_MAP: continue add_compiler_if_missing(infos, langname, exes) def detect_missing_native_binaries(infos: MachineInfo) -> None: # T.Any per-platform special detection should go here. for toolname in sorted(envconfig.ENV_VAR_TOOL_MAP.keys()): if toolname in infos.binaries: continue exe = shutil.which(toolname) if exe: infos.binaries[toolname] = [exe] def detect_native_env(options: T.Any) -> MachineInfo: use_for_build = has_for_build() if use_for_build: mlog.log('Using FOR_BUILD envvars for detection') esuffix = '_FOR_BUILD' else: mlog.log('Using regular envvars for detection.') esuffix = '' infos = detect_compilers_from_envvars(esuffix) detect_missing_native_compilers(infos) detect_binaries_from_envvars(infos, esuffix) detect_missing_native_binaries(infos) detect_properties_from_envvars(infos, esuffix) return infos def run(options: T.Any) -> None: if options.cross and options.native: sys.exit('You can only specify either --cross or --native, not both.') if not options.cross and not options.native: sys.exit('You must specify --cross or --native.') mlog.notice('This functionality is experimental and subject to change.') detect_cross = options.cross if detect_cross: infos = detect_cross_env(options) write_system_info = True else: infos = detect_native_env(options) write_system_info = False write_machine_file(infos, options.outfile, write_system_info)