| # SPDX-License-Identifier: Apache-2.0 |
| # Copyright 2022 The Meson development team |
| |
| from __future__ import annotations |
| |
| 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.') |
| |
| class MachineInfo: |
| def __init__(self) -> None: |
| self.compilers: T.Dict[str, T.List[str]] = {} |
| self.binaries: T.Dict[str, T.List[str]] = {} |
| self.properties: T.Dict[str, T.Union[str, T.List[str]]] = {} |
| self.compile_args: T.Dict[str, T.List[str]] = {} |
| self.link_args: T.Dict[str, T.List[str]] = {} |
| self.cmake: T.Dict[str, T.Union[str, T.List[str]]] = {} |
| |
| self.system: T.Optional[str] = None |
| self.subsystem: T.Optional[str] = None |
| self.kernel: T.Optional[str] = None |
| self.cpu: T.Optional[str] = None |
| self.cpu_family: T.Optional[str] = None |
| self.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 |
| |
| deb_cpu_family_map = { |
| 'mips64el': 'mips64', |
| 'i686': 'x86', |
| 'powerpc64le': 'ppc64', |
| } |
| |
| deb_cpu_map = { |
| 'armhf': 'arm7hlf', |
| 'mips64el': 'mips64', |
| 'powerpc64le': 'ppc64', |
| } |
| |
| 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"] = system_processor_map.get(data['DEB_HOST_GNU_CPU'], |
| 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) |
| 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 = data['DEB_HOST_ARCH_OS'] |
| host_subsystem = host_os |
| host_kernel = 'linux' |
| host_cpu_family = deb_cpu_family_map.get(data['DEB_HOST_GNU_CPU'], |
| data['DEB_HOST_GNU_CPU']) |
| host_cpu = deb_cpu_map.get(data['DEB_HOST_ARCH'], |
| data['DEB_HOST_ARCH']) |
| 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 |
| try: |
| infos.binaries['pkg-config'] = locate_path("%s-pkg-config" % host_arch) |
| except ValueError: |
| pass # pkg-config is optional |
| 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-reconfigure.') |
| 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) |