# Copyright 2022 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. from __future__ import annotations import sys, os, subprocess, shutil import shlex import typing as T from .. import mlog if T.TYPE_CHECKING: import argparse UNIXY_ENVVARS_COMPILER = {'c': 'CC', 'cpp': 'CXX', 'objc': 'OBJCC', 'objcpp': 'OBJCXX', 'fortran': 'FC', 'rust': 'RUSTC', 'vala': 'VALAC', 'cs': 'CSC', } UNIXY_ENVVARS_TOOLS = {'ar': 'AR', 'strip': 'STRIP', 'windres': 'WINDRES', 'pkgconfig': 'PKG_CONFIG', 'vapigen': 'VAPIGEN', 'cmake': 'CMAKE', 'qmake': 'QMAKE', } UNIXY_ENVVARS_FLAGS = {'c': 'CFLAGS', 'cpp': 'CXXFLAGS', 'objc': 'OBJCFLAGS', 'objcpp': 'OBJCXXFLAGS', 'fortran': 'FFLAGS', 'rust': 'RUSTFLAGS', 'vala': 'VALAFLAGS', 'cs': 'CSFLAGS', # This one might not be standard. } TYPICAL_UNIXY_COMPILER_NAMES = {'c': ['cc', 'gcc', 'clang'], 'cpp': ['c++', 'g++', 'clang++'], 'objc': ['objc', 'clang'], 'objcpp': ['objcpp', 'clang++'], 'fortran': ['gfortran'], } LANGS_USING_CPPFLAGS = {'c', 'cpp', 'objc', 'objcxx'} def has_for_build() -> bool: for cenv in UNIXY_ENVVARS_COMPILER.values(): if os.environ.get(cenv + '_FOR_BUILD'): return True return False 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('--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.system: 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.List[str]) -> None: if len(args) == 0: return 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 cpu_family_map = dict(mips64el="mips64", i686='x86') cpu_map = dict(armhf="arm7hlf", mips64el="mips64",) 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 is None: 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_cpu_family = cpu_family_map.get(data['DEB_HOST_GNU_CPU'], data['DEB_HOST_GNU_CPU']) host_cpu = 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['pkgconfig'] = 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.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') 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('[properties]\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') 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") os.replace(tmpfilename, ofilename) def detect_language_args_from_envvars(langname: str, envvar_suffix: str = '') -> T.Tuple[T.List[str], T.List[str]]: ldflags = tuple(shlex.split(os.environ.get('LDFLAGS' + envvar_suffix, ''))) compile_args = shlex.split(os.environ.get(UNIXY_ENVVARS_FLAGS[langname] + envvar_suffix, '')) if langname in LANGS_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 = list(ldflags) + compile_args return (lang_compile_args, lang_link_args) def detect_compilers_from_envvars(envvar_suffix: str = '') -> MachineInfo: infos = MachineInfo() for langname, envvarname in UNIXY_ENVVARS_COMPILER.items(): compilerstr = os.environ.get(envvarname + envvar_suffix) if not compilerstr: continue 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 UNIXY_ENVVARS_TOOLS.items(): envvar = envvar_base + envvar_suffix binstr = os.environ.get(envvar) if binstr: infos.binaries[binname] = shlex.split(binstr) def detect_cross_system(infos: MachineInfo, options: T.Any) -> None: for optname in ('system', '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) 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 TYPICAL_UNIXY_COMPILER_NAMES.items(): 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(UNIXY_ENVVARS_TOOLS.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) 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)