# SPDX-License-Identifier: Apache-2.0
# Copyright 2016-2021 The Meson development team

import subprocess
import re
import os
import platform
import unittest

from mesonbuild.mesonlib import (
    MachineChoice, is_osx, version_compare
)
from mesonbuild.compilers import (
    detect_c_compiler
)


from run_tests import (
    get_fake_env
)

from .baseplatformtests import BasePlatformTests
from .helpers import *

@unittest.skipUnless(is_osx(), "requires Darwin")
class DarwinTests(BasePlatformTests):
    '''
    Tests that should run on macOS
    '''

    def setUp(self):
        super().setUp()
        self.platform_test_dir = os.path.join(self.src_root, 'test cases/osx')

    def test_apple_bitcode(self):
        '''
        Test that -fembed-bitcode is correctly added while compiling and
        -bitcode_bundle is added while linking when b_bitcode is true and not
        when it is false.  This can't be an ordinary test case because we need
        to inspect the compiler database.
        '''
        testdir = os.path.join(self.platform_test_dir, '7 bitcode')
        env = get_fake_env(testdir, self.builddir, self.prefix)
        cc = detect_c_compiler(env, MachineChoice.HOST)
        if cc.id != 'clang':
            raise unittest.SkipTest('Not using Clang on OSX')
        # Try with bitcode enabled
        out = self.init(testdir, extra_args='-Db_bitcode=true')
        # Warning was printed
        self.assertRegex(out, 'WARNING:.*b_bitcode')
        # Compiler options were added
        for compdb in self.get_compdb():
            if 'module' in compdb['file']:
                self.assertNotIn('-fembed-bitcode', compdb['command'])
            else:
                self.assertIn('-fembed-bitcode', compdb['command'])
        build_ninja = os.path.join(self.builddir, 'build.ninja')
        # Linker options were added
        with open(build_ninja, encoding='utf-8') as f:
            contents = f.read()
            m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
        self.assertIsNotNone(m, msg=contents)
        # Try with bitcode disabled
        self.setconf('-Db_bitcode=false')
        # Regenerate build
        self.build()
        for compdb in self.get_compdb():
            self.assertNotIn('-fembed-bitcode', compdb['command'])
        build_ninja = os.path.join(self.builddir, 'build.ninja')
        with open(build_ninja, encoding='utf-8') as f:
            contents = f.read()
            m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
        self.assertIsNone(m, msg=contents)

    def test_apple_bitcode_modules(self):
        '''
        Same as above, just for shared_module()
        '''
        testdir = os.path.join(self.common_test_dir, '148 shared module resolving symbol in executable')
        # Ensure that it builds even with bitcode enabled
        self.init(testdir, extra_args='-Db_bitcode=true')
        self.build()
        self.run_tests()

    @unittest.skipIf(version_compare(platform.mac_ver()[0], '<10.7'), '-export_dynamic was added in 10.7')
    def test_apple_lto_export_dynamic(self):
        '''
        Tests that -Wl,-export_dynamic is correctly added, when export_dynamic: true is set.
        On macOS, this is relevant for LTO builds only.
        '''
        testdir = os.path.join(self.common_test_dir, '148 shared module resolving symbol in executable')
        # Ensure that it builds even with LTO enabled
        env = {'CFLAGS': '-flto'}
        self.init(testdir, override_envvars=env)
        self.build()
        self.run_tests()

    def _get_darwin_versions(self, fname):
        fname = os.path.join(self.builddir, fname)
        out = subprocess.check_output(['otool', '-L', fname], universal_newlines=True)
        m = re.match(r'.*version (.*), current version (.*)\)', out.split('\n')[1])
        self.assertIsNotNone(m, msg=out)
        return m.groups()

    def _get_darwin_rpaths(self, fname: str) -> T.List[str]:
        out = subprocess.check_output(['otool', '-l', fname], universal_newlines=True)
        pattern = re.compile(r'path (.*) \(offset \d+\)')
        rpaths = pattern.findall(out)
        return rpaths

    @skipIfNoPkgconfig
    def test_library_versioning(self):
        '''
        Ensure that compatibility_version and current_version are set correctly
        '''
        testdir = os.path.join(self.platform_test_dir, '2 library versions')
        self.init(testdir)
        self.build()
        targets = {}
        for t in self.introspect('--targets'):
            targets[t['name']] = t['filename'][0] if isinstance(t['filename'], list) else t['filename']
        self.assertEqual(self._get_darwin_versions(targets['some']), ('7.0.0', '7.0.0'))
        self.assertEqual(self._get_darwin_versions(targets['noversion']), ('0.0.0', '0.0.0'))
        self.assertEqual(self._get_darwin_versions(targets['onlyversion']), ('1.0.0', '1.0.0'))
        self.assertEqual(self._get_darwin_versions(targets['onlysoversion']), ('5.0.0', '5.0.0'))
        self.assertEqual(self._get_darwin_versions(targets['intver']), ('2.0.0', '2.0.0'))
        self.assertEqual(self._get_darwin_versions(targets['stringver']), ('2.3.0', '2.3.0'))
        self.assertEqual(self._get_darwin_versions(targets['stringlistver']), ('2.4.0', '2.4.0'))
        self.assertEqual(self._get_darwin_versions(targets['intstringver']), ('1111.0.0', '2.5.0'))
        self.assertEqual(self._get_darwin_versions(targets['stringlistvers']), ('2.6.0', '2.6.1'))

    def test_duplicate_rpath(self):
        testdir = os.path.join(self.unit_test_dir, '10 build_rpath')
        # We purposely pass a duplicate rpath to Meson, in order
        # to ascertain that Meson does not call install_name_tool
        # with duplicate -delete_rpath arguments, which would
        # lead to erroring out on installation
        env = {"LDFLAGS": "-Wl,-rpath,/foo/bar"}
        self.init(testdir, override_envvars=env)
        self.build()
        self.install()

    def test_removing_unused_linker_args(self):
        testdir = os.path.join(self.common_test_dir, '104 has arg')
        env = {'CFLAGS': '-L/tmp -L /var/tmp -headerpad_max_install_names -Wl,-export_dynamic -framework Foundation'}
        self.init(testdir, override_envvars=env)

    def test_objc_versions(self):
        # Objective-C always uses the C standard version.
        # Objective-C++ always uses the C++ standard version.
        # This is what most people seem to want and in addition
        # it is the only setup supported by Xcode.
        testdir = os.path.join(self.objc_test_dir, '1 simple')
        self.init(testdir)
        self.assertIn('-std=c99', self.get_compdb()[0]['command'])
        self.wipe()
        testdir = os.path.join(self.objcpp_test_dir, '1 simple')
        self.init(testdir)
        self.assertIn('-std=c++14', self.get_compdb()[0]['command'])

    def test_darwin_get_object_archs(self):
        from mesonbuild.mesonlib import darwin_get_object_archs
        archs = darwin_get_object_archs('/bin/cat')
        self.assertEqual(archs, ['x86_64', 'aarch64'])

    def test_darwin_meson_rpaths_removed_on_install(self):
        testdir = os.path.join(self.darwin_test_dir, '1 rpath removal on install')
        self.init(testdir)
        self.build()
        # Meson-created RPATHs are usually only valid in the build directory
        rpaths = self._get_darwin_rpaths(os.path.join(self.builddir, 'libbar.dylib'))
        self.assertListEqual(rpaths, ['@loader_path/foo'])
        self.install()
        # Those RPATHs are no longer valid and should not be present after installation
        rpaths = self._get_darwin_rpaths(os.path.join(self.installdir, 'usr/lib/libbar.dylib'))
        self.assertListEqual(rpaths, [])

    @skip_if_not_language('rust')
    def test_rust_apple_framework_rlib(self):
        '''
        Test that Rust rlibs properly record Apple framework dependencies,
        so that external tools (like cargo) can link against them without
        meson's help.
        '''
        testdir = os.path.join(self.rust_test_dir, '37 apple framework')
        self.init(testdir)
        # Build only the library, not the executable
        self.build(target='timelib')
        # Manually invoke rustc to build the executable, using the rlib.
        # This simulates what cargo or another build system would do.
        rlib = os.path.join(self.builddir, 'libtimelib.rlib')
        main_rs = os.path.join(testdir, 'main.rs')
        out_exe = os.path.join(self.builddir, 'manual_main')
        subprocess.check_call(['rustc', '--extern', f'timelib={rlib}', main_rs, '-o', out_exe])
        # Run the executable to verify it works
        subprocess.check_call([out_exe])
