blob: bc0101e26d5b98c76e23c36563231603d37a4fbd [file] [log] [blame]
# 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)