# SPDX-License-Identifier: Apache-2.0
# Copyright 2012-2022 The Meson development team

from __future__ import annotations

from .base import RSPFileSyntax
from .. import mlog
from ..mesonlib import (
    EnvironmentException,
    Popen_safe, Popen_safe_logged, join_args, search_version
)

import re
import shlex
import typing as T

if T.TYPE_CHECKING:
    from .linkers import DynamicLinker, GnuDynamicLinker
    from ..environment import Environment
    from ..compilers import Compiler
    from ..mesonlib import MachineChoice

defaults: T.Dict[str, T.List[str]] = {}
defaults['static_linker'] = ['ar', 'gar']
defaults['vs_static_linker'] = ['lib']
defaults['clang_cl_static_linker'] = ['llvm-lib']
defaults['cuda_static_linker'] = ['nvlink']
defaults['gcc_static_linker'] = ['gcc-ar']
defaults['clang_static_linker'] = ['llvm-ar']
defaults['emxomf_static_linker'] = ['emxomfar']

def __failed_to_detect_linker(compiler: T.List[str], args: T.List[str], stdout: str, stderr: str) -> 'T.NoReturn':
    msg = 'Unable to detect linker for compiler `{}`\nstdout: {}\nstderr: {}'.format(
        join_args(compiler + args), stdout, stderr)
    raise EnvironmentException(msg)


def guess_win_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Type['Compiler'],
                     comp_version: str, for_machine: MachineChoice, *,
                     use_linker_prefix: bool = True, invoked_directly: bool = True,
                     extra_args: T.Optional[T.List[str]] = None) -> 'DynamicLinker':
    from . import linkers
    env.add_lang_args(comp_class.language, comp_class, for_machine)

    if invoked_directly or comp_class.get_argument_syntax() == 'msvc':
        rsp_syntax = RSPFileSyntax.MSVC
    else:
        rsp_syntax = RSPFileSyntax.GCC

    # Explicitly pass logo here so that we can get the version of link.exe
    if not use_linker_prefix or comp_class.LINKER_PREFIX is None:
        check_args = ['/logo', '--version']
    elif isinstance(comp_class.LINKER_PREFIX, str):
        check_args = [comp_class.LINKER_PREFIX + '/logo', comp_class.LINKER_PREFIX + '--version']
    else: # list
        check_args = comp_class.LINKER_PREFIX + ['/logo'] + comp_class.LINKER_PREFIX + ['--version']

    check_args += env.coredata.get_external_link_args(for_machine, comp_class.language)

    override: T.List[str] = []
    value = env.lookup_binary_entry(for_machine, comp_class.language + '_ld')
    if value is not None:
        override = comp_class.use_linker_args(value[0], comp_version)
        check_args += override
    elif 'lld-link' in compiler:
        override = comp_class.use_linker_args('lld-link', comp_version)
        check_args += override

    if extra_args is not None:
        check_args.extend(extra_args)

    p, o, _ = Popen_safe(compiler + check_args)
    if 'LLD' in o.split('\n', maxsplit=1)[0]:
        if 'compatible with GNU linkers' in o:
            return linkers.LLVMDynamicLinker(
                compiler, env, for_machine, comp_class.LINKER_PREFIX,
                override, version=search_version(o))
        elif not invoked_directly:
            return linkers.ClangClDynamicLinker(
                env, for_machine, override, exelist=compiler, prefix=comp_class.LINKER_PREFIX,
                version=search_version(o), direct=False, machine=None,
                rsp_syntax=rsp_syntax)

    if value is not None and invoked_directly:
        compiler = value
        # We've already handled the non-direct case above

    p, o, e = Popen_safe(compiler + check_args)
    if 'LLD' in o.split('\n', maxsplit=1)[0]:
        return linkers.ClangClDynamicLinker(
            env, for_machine, [],
            prefix=comp_class.LINKER_PREFIX if use_linker_prefix else [],
            exelist=compiler, version=search_version(o), direct=invoked_directly,
            rsp_syntax=rsp_syntax)
    elif 'OPTLINK' in o:
        # Optlink's stdout *may* begin with a \r character.
        return linkers.OptlinkDynamicLinker(compiler, env, for_machine, version=search_version(o))
    elif o.startswith('Microsoft') or e.startswith('Microsoft'):
        out = o or e
        match = re.search(r'.*(X86|X64|ARM|ARM64).*', out)
        if match:
            target = str(match.group(1))
        else:
            target = 'x86'

        return linkers.MSVCDynamicLinker(
            env, for_machine, [], machine=target, exelist=compiler,
            prefix=comp_class.LINKER_PREFIX if use_linker_prefix else [],
            version=search_version(out), direct=invoked_directly,
            rsp_syntax=rsp_syntax)
    elif 'GNU coreutils' in o:
        import shutil
        fullpath = shutil.which(compiler[0])
        raise EnvironmentException(
            f"Found GNU link.exe instead of MSVC link.exe in {fullpath}.\n"
            "This link.exe is not a linker.\n"
            "You may need to reorder entries to your %PATH% variable to resolve this.")
    __failed_to_detect_linker(compiler, check_args, o, e)

def guess_nix_linker(env: 'Environment', compiler: T.List[str], comp_class: T.Type['Compiler'],
                     comp_version: str, for_machine: MachineChoice, *,
                     extra_args: T.Optional[T.List[str]] = None) -> 'DynamicLinker':
    """Helper for guessing what linker to use on Unix-Like OSes.

    :compiler: Invocation to use to get linker
    :comp_class: The Compiler Type (uninstantiated)
    :comp_version: The compiler version string
    :for_machine: which machine this linker targets
    :extra_args: Any additional arguments required (such as a source file)
    """
    from . import linkers
    from ..options import OptionKey
    env.add_lang_args(comp_class.language, comp_class, for_machine)
    extra_args = extra_args or []

    system = env.machines[for_machine].system
    ldflags = env.coredata.get_external_link_args(for_machine, comp_class.language)
    extra_args += comp_class._unix_args_to_native(ldflags, env.machines[for_machine])

    if isinstance(comp_class.LINKER_PREFIX, str):
        check_args = [comp_class.LINKER_PREFIX + '--version'] + extra_args
    else:
        check_args = comp_class.LINKER_PREFIX + ['--version'] + extra_args

    override: T.List[str] = []
    value = env.lookup_binary_entry(for_machine, comp_class.language + '_ld')
    if value is not None:
        override = comp_class.use_linker_args(value[0], comp_version)
        check_args += override

    if env.machines[for_machine].is_os2() and env.coredata.optstore.get_value_for(OptionKey('os2_emxomf')):
        check_args += ['-Zomf']

    mlog.debug('-----')
    p, o, e = Popen_safe_logged(compiler + check_args, msg='Detecting linker via')

    v = search_version(o + e)
    linker: DynamicLinker
    if 'LLD' in o.split('\n', maxsplit=1)[0] or 'tiarmlnk' in e:
        if isinstance(comp_class.LINKER_PREFIX, str):
            cmd = compiler + override + [comp_class.LINKER_PREFIX + '-v'] + extra_args
        else:
            cmd = compiler + override + comp_class.LINKER_PREFIX + ['-v'] + extra_args
        _, newo, newerr = Popen_safe_logged(cmd, msg='Detecting LLD linker via')

        lld_cls: T.Type[DynamicLinker]
        if 'ld64.lld' in newerr:
            lld_cls = linkers.LLVMLD64DynamicLinker
        else:
            lld_cls = linkers.LLVMDynamicLinker

        linker = lld_cls(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override, system=system, version=v)
    elif o.startswith("eld"):
        linker = linkers.ELDDynamicLinker(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override, version=v)
    elif 'Snapdragon' in e and 'LLVM' in e:
        linker = linkers.QualcommLLVMDynamicLinker(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override, version=v)
    elif e.startswith('lld-link: '):
        # The LLD MinGW frontend didn't respond to --version before version 9.0.0,
        # and produced an error message about failing to link (when no object
        # files were specified), instead of printing the version number.
        # Let's try to extract the linker invocation command to grab the version.

        _, o, e = Popen_safe(compiler + check_args + ['-v'])

        try:
            linker_cmd = re.match(r'.*\n(.*?)\nlld-link: ', e, re.DOTALL).group(1)
            linker_cmd = shlex.split(linker_cmd)[0]
        except (AttributeError, IndexError, ValueError):
            pass
        else:
            _, o, e = Popen_safe([linker_cmd, '--version'])
            v = search_version(o)

        linker = linkers.LLVMDynamicLinker(compiler, env, for_machine, comp_class.LINKER_PREFIX, override, version=v)
    elif 'GNU' in o or 'GNU' in e:
        gnu_cls: T.Type[GnuDynamicLinker]
        # this is always the only thing on stdout, except for swift
        # which may or may not redirect the linker stdout to stderr
        if o.startswith('GNU gold') or e.startswith('GNU gold'):
            gnu_cls = linkers.GnuGoldDynamicLinker
        elif o.startswith('mold') or e.startswith('mold'):
            gnu_cls = linkers.MoldDynamicLinker
        else:
            gnu_cls = linkers.GnuBFDDynamicLinker
        linker = gnu_cls(compiler, env, for_machine, comp_class.LINKER_PREFIX, override, version=v)
    elif 'Solaris' in e or 'Solaris' in o:
        for line in (o+e).split('\n'):
            if 'ld: Software Generation Utilities' in line:
                v = line.split(':')[2].lstrip()
                break
        else:
            v = 'unknown version'
        linker = linkers.SolarisDynamicLinker(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override,
            version=v)
    elif 'ld: 0706-012 The -- flag is not recognized' in e:
        if isinstance(comp_class.LINKER_PREFIX, str):
            _, _, e = Popen_safe(compiler + [comp_class.LINKER_PREFIX + '-V'] + extra_args)
        else:
            _, _, e = Popen_safe(compiler + comp_class.LINKER_PREFIX + ['-V'] + extra_args)
        linker = linkers.AIXDynamicLinker(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override,
            version=search_version(e))
    elif o.startswith('zig ld'):
        linker = linkers.ZigCCDynamicLinker(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override, version=v)
    # detect xtools first, bug #10805
    elif 'xtools-' in o.split('\n', maxsplit=1)[0]:
        xtools = o.split(' ', maxsplit=1)[0]
        v = xtools.split('-', maxsplit=2)[1]
        linker = linkers.AppleDynamicLinker(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override,
            system=system, version=v
        )
    # detect linker on MacOS - must be after other platforms because the
    # "(use -v to see invocation)" will match clang on other platforms,
    # but the rest of the checks will fail and call __failed_to_detect_linker.
    # First might be apple clang, second is for real gcc, the third is icc.
    # Note that "ld: unknown option: " sometimes instead is "ld: unknown options:".
    elif e.endswith('(use -v to see invocation)\n') or 'macosx_version' in e or 'ld: unknown option' in e:
        if isinstance(comp_class.LINKER_PREFIX, str):
            cmd = compiler + [comp_class.LINKER_PREFIX + '-v'] + extra_args
        else:
            cmd = compiler + comp_class.LINKER_PREFIX + ['-v'] + extra_args
        _, newo, newerr = Popen_safe_logged(cmd, msg='Detecting Apple linker via')

        for line in newerr.split('\n'):
            if 'PROJECT:ld' in line or 'PROJECT:dyld' in line:
                v = line.split('-')[1]
                break
        else:
            __failed_to_detect_linker(compiler, check_args, o, e)
        linker = linkers.AppleDynamicLinker(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override,
            system=system, version=v
        )
    elif 'ld.exe: unrecognized option' in e:
        linker = linkers.OS2AoutDynamicLinker(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override,
            version='none')
    elif 'emxomfld: invalid option' in e:
        linker = linkers.OS2OmfDynamicLinker(
            compiler, env, for_machine, comp_class.LINKER_PREFIX, override,
            version='none')
    else:
        __failed_to_detect_linker(compiler, check_args, o, e)
    return linker
# SPDX-License-Identifier: Apache-2.0
# Copyright 2013-2024 Contributors to the The Meson project
# Copyright © 2024 Intel Corporation

from __future__ import annotations
import typing as T
import configparser
import os

from . import mparser

from .cmdline import CmdLineFileParser
from .mesonlib import MesonException

if T.TYPE_CHECKING:
    from .options import ElementaryOptionValues

class MachineFileParser():
    def __init__(self, filenames: T.List[str], sourcedir: str) -> None:
        self.parser = CmdLineFileParser()
        self.constants: T.Dict[str, ElementaryOptionValues] = {'True': True, 'False': False}
        self.sections: T.Dict[str, T.Dict[str, ElementaryOptionValues]] = {}

        for fname in filenames:
            try:
                with open(fname, encoding='utf-8') as f:
                    content = f.read()
            except UnicodeDecodeError as e:
                raise MesonException(f'Malformed machine file {fname!r} failed to parse as unicode: {e}')

            content = content.replace('@GLOBAL_SOURCE_ROOT@', sourcedir)
            content = content.replace('@DIRNAME@', os.path.dirname(fname))
            try:
                self.parser.read_string(content, fname)
            except configparser.Error as e:
                raise MesonException(f'Malformed machine file: {e}')

        # Parse [constants] first so they can be used in other sections
        if self.parser.has_section('constants'):
            self.constants.update(self._parse_section('constants'))

        for s in self.parser.sections():
            if s == 'constants':
                continue
            self.sections[s] = self._parse_section(s)

    def _parse_section(self, s: str) -> T.Dict[str, ElementaryOptionValues]:
        self.scope = self.constants.copy()
        section: T.Dict[str, ElementaryOptionValues] = {}
        for entry, value in self.parser.items(s):
            if ' ' in entry or '\t' in entry or "'" in entry or '"' in entry:
                raise MesonException(f'Malformed variable name {entry!r} in machine file.')
            # Windows paths...
            value = value.replace('\\', '\\\\')
            try:
                ast = mparser.Parser(value, 'machinefile').parse()
                if not ast.lines:
                    raise MesonException('value cannot be empty')
                res = self._evaluate_statement(ast.lines[0])
            except MesonException as e:
                raise MesonException(f'Malformed value in machine file variable {entry!r}: {str(e)}.')
            except KeyError as e:
                raise MesonException(f'Undefined constant {e.args[0]!r} in machine file variable {entry!r}.')
            section[entry] = res
            self.scope[entry] = res
        return section

    def _evaluate_statement(self, node: mparser.BaseNode) -> ElementaryOptionValues:
        if isinstance(node, (mparser.StringNode)):
            return node.value
        elif isinstance(node, mparser.BooleanNode):
            return node.value
        elif isinstance(node, mparser.NumberNode):
            return node.value
        elif isinstance(node, mparser.ParenthesizedNode):
            return self._evaluate_statement(node.inner)
        elif isinstance(node, mparser.ArrayNode):
            a = [self._evaluate_statement(arg) for arg in node.args.arguments]
            assert all(isinstance(s, str) for s in a), 'for mypy'
            return T.cast('T.List[str]', a)
        elif isinstance(node, mparser.IdNode):
            return self.scope[node.value]
        elif isinstance(node, mparser.ArithmeticNode):
            l = self._evaluate_statement(node.left)
            r = self._evaluate_statement(node.right)
            if node.operation == 'add':
                if isinstance(l, str) and isinstance(r, str):
                    return l + r
                if isinstance(l, list) and isinstance(r, list):
                    return l + r
            elif node.operation == 'div':
                if isinstance(l, str) and isinstance(r, str):
                    return os.path.join(l, r)
        raise MesonException('Unsupported node type')

def parse_machine_files(filenames: T.List[str], sourcedir: str) -> T.Dict[str, T.Dict[str, ElementaryOptionValues]]:
    parser = MachineFileParser(filenames, sourcedir)
    return parser.sections


class MachineFileStore:
    def __init__(self, native_files: T.Optional[T.List[str]], cross_files: T.Optional[T.List[str]], source_dir: str):
        self.native = parse_machine_files(native_files if native_files is not None else [], source_dir)
        self.cross = parse_machine_files(cross_files if cross_files is not None else [], source_dir)
