Repository: conan-io/conan

Issue:
Suggestion: adding a helper for darwin host
Hi,

I was wondering if a helper could be added in `tools` to detect if the `os` setting is targeting a darwin platform. Here is my use-case:

```python
    if self.settings.os in ["Macos", "iOS", "watchOS",  "tvOS"]:
        # ...

    # What I have in mind
    if tools.darwin_platform(self.settings.os):
        # ...
```

This is not a big deal, but I'm too lazy to type it in every recipe :D


Discussion:

Resolving patch:
diff --git a/conans/client/tools/__init__.py b/conans/client/tools/__init__.py
--- a/conans/client/tools/__init__.py
+++ b/conans/client/tools/__init__.py
@@ -13,3 +13,5 @@
 from .win import *
 # noinspection PyUnresolvedReferences
 from .pkg_config import *
+# noinspection PyUnresolvedReferences
+from .apple import *
diff --git a/conans/client/tools/apple.py b/conans/client/tools/apple.py
new file mode 100644
--- /dev/null
+++ b/conans/client/tools/apple.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import subprocess
+
+
+def is_apple_os(os_):
+    """returns True if OS is Apple one (Macos, iOS, watchOS or tvOS"""
+    return str(os_) in ['Macos', 'iOS', 'watchOS', 'tvOS']
+
+
+def to_apple_arch(arch):
+    """converts conan-style architecture into Apple-style arch"""
+    return {'x86': 'i386',
+            'x86_64': 'x86_64',
+            'armv7': 'armv7',
+            'armv8': 'arm64',
+            'armv7s': 'armv7s',
+            'armv7k': 'armv7k'}.get(str(arch))
+
+
+def apple_sdk_name(settings):
+    """returns proper SDK name suitable for OS and architecture
+    we're building for (considering simulators)"""
+    arch = settings.get_safe('arch')
+    os_ = settings.get_safe('os')
+    if str(arch).startswith('x86'):
+        return {'Macos': 'macosx',
+                'iOS': 'iphonesimulator',
+                'watchOS': 'watchsimulator',
+                'tvOS': 'appletvsimulator'}.get(str(os_))
+    elif str(arch).startswith('arm'):
+        return {'iOS': 'iphoneos',
+                'watchOS': 'watchos',
+                'tvOS': 'appletvos'}.get(str(os_))
+    else:
+        return None
+
+
+def apple_deployment_target_env(os_, os_version):
+    """environment variable name which controls deployment target"""
+    env_name = {'Macos': 'MACOSX_DEPLOYMENT_TARGET',
+                'iOS': 'IOS_DEPLOYMENT_TARGET',
+                'watchOS': 'WATCHOS_DEPLOYMENT_TARGET',
+                'tvOS': 'TVOS_DEPLOYMENT_TARGET'}.get(str(os_))
+    if not env_name:
+        return {}
+    return {env_name: os_version}
+
+
+def apple_deployment_target_flag(os_, os_version):
+    """compiler flag name which controls deployment target"""
+    flag = {'Macos': '-mmacosx-version-min',
+            'iOS': '-mios-version-min',
+            'watchOS': '-mwatchos-version-min',
+            'tvOS': '-mappletvos-version-min'}.get(str(os_))
+    if not flag:
+        return ''
+    return "%s=%s" % (flag, os_version)
+
+
+class XCRun(object):
+
+    def __init__(self, settings, sdk=None):
+        """sdk=False will skip the flag
+           sdk=None will try to adjust it automatically"""
+        if sdk is None and settings:
+            sdk = apple_sdk_name(settings)
+        self.sdk = sdk
+
+    def _invoke(self, args):
+        def cmd_output(cmd):
+            return subprocess.check_output(cmd).decode().strip()
+
+        command = ['xcrun', '-find']
+        if self.sdk:
+            command.extend(['-sdk', self.sdk])
+        command.extend(args)
+        return cmd_output(command)
+
+    def find(self, tool):
+        """find SDK tools (e.g. clang, ar, ranlib, lipo, codesign, etc.)"""
+        return self._invoke(['--find', tool])
+
+    @property
+    def sdk_path(self):
+        """obtain sdk path (aka apple sysroot or -isysroot"""
+        return self._invoke(['--show-sdk-path'])
+
+    @property
+    def sdk_version(self):
+        """obtain sdk version"""
+        return self._invoke(['--show-sdk-version'])
+
+    @property
+    def sdk_platform_path(self):
+        """obtain sdk platform path"""
+        return self._invoke(['--show-sdk-platform-path'])
+
+    @property
+    def sdk_platform_version(self):
+        """obtain sdk platform version"""
+        return self._invoke(['--show-sdk-platform-version'])
+
+    @property
+    def cc(self):
+        """path to C compiler (CC)"""
+        return self.find('clang')
+
+    @property
+    def cxx(self):
+        """path to C++ compiler (CXX)"""
+        return self.find('clang++')
+
+    @property
+    def ar(self):
+        """path to archiver (AR)"""
+        return self.find('ar')
+
+    @property
+    def ranlib(self):
+        """path to archive indexer (RANLIB)"""
+        return self.find('ranlib')
+
+    @property
+    def strip(self):
+        """path to symbol removal utility (STRIP)"""
+        return self.find('strip')


Test patch:
diff --git a/conans/test/util/apple_test.py b/conans/test/util/apple_test.py
new file mode 100644
--- /dev/null
+++ b/conans/test/util/apple_test.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import unittest
+import platform
+import os
+from conans import tools
+
+
+class FakeSettings(object):
+    def __init__(self, _os, _arch):
+        self._os = _os
+        self._arch = _arch
+
+    def get_safe(self, name):
+        if name == 'os':
+            return self._os
+        elif name == 'arch':
+            return self._arch
+
+
+class AppleTest(unittest.TestCase):
+    def test_is_apple_os(self):
+        self.assertTrue(tools.is_apple_os('iOS'))
+        self.assertTrue(tools.is_apple_os('tvOS'))
+        self.assertTrue(tools.is_apple_os('watchOS'))
+        self.assertTrue(tools.is_apple_os('Macos'))
+        self.assertFalse(tools.is_apple_os('Windows'))
+        self.assertFalse(tools.is_apple_os('Linux'))
+        self.assertFalse(tools.is_apple_os('Android'))
+
+    def test_to_apple_arch(self):
+        self.assertEqual(tools.to_apple_arch('x86'), 'i386')
+        self.assertEqual(tools.to_apple_arch('x86_64'), 'x86_64')
+        self.assertEqual(tools.to_apple_arch('armv7'), 'armv7')
+        self.assertEqual(tools.to_apple_arch('armv7s'), 'armv7s')
+        self.assertEqual(tools.to_apple_arch('armv7k'), 'armv7k')
+        self.assertEqual(tools.to_apple_arch('armv8'), 'arm64')
+        self.assertIsNone(tools.to_apple_arch('mips'))
+
+    def test_apple_sdk_name(self):
+
+        self.assertEqual(tools.apple_sdk_name(FakeSettings('Macos', 'x86')), 'macosx')
+        self.assertEqual(tools.apple_sdk_name(FakeSettings('Macos', 'x86_64')), 'macosx')
+        self.assertEqual(tools.apple_sdk_name(FakeSettings('iOS', 'x86_64')), 'iphonesimulator')
+        self.assertEqual(tools.apple_sdk_name(FakeSettings('iOS', 'armv7')), 'iphoneos')
+        self.assertEqual(tools.apple_sdk_name(FakeSettings('watchOS', 'x86_64')), 'watchsimulator')
+        self.assertEqual(tools.apple_sdk_name(FakeSettings('watchOS', 'armv7k')), 'watchos')
+        self.assertEqual(tools.apple_sdk_name(FakeSettings('tvOS', 'x86')), 'appletvsimulator')
+        self.assertEqual(tools.apple_sdk_name(FakeSettings('tvOS', 'armv8')), 'appletvos')
+        self.assertIsNone(tools.apple_sdk_name(FakeSettings('Windows', 'x86')))
+
+    def test_deployment_target_env_name(self):
+        self.assertEqual(tools.apple_deployment_target_env('Macos', "10.1"),
+                         {"MACOSX_DEPLOYMENT_TARGET": "10.1"})
+        self.assertEqual(tools.apple_deployment_target_env('iOS', "10.1"),
+                         {"IOS_DEPLOYMENT_TARGET": "10.1"})
+        self.assertEqual(tools.apple_deployment_target_env('watchOS', "10.1"),
+                         {"WATCHOS_DEPLOYMENT_TARGET": "10.1"})
+        self.assertEqual(tools.apple_deployment_target_env('tvOS', "10.1"),
+                         {"TVOS_DEPLOYMENT_TARGET": "10.1"})
+        self.assertEqual(tools.apple_deployment_target_env('Linux', "10.1"), {})
+
+    def test_deployment_target_flag_name(self):
+        self.assertEqual(tools.apple_deployment_target_flag('Macos', "10.1"),
+                         '-mmacosx-version-min=10.1')
+        self.assertEqual(tools.apple_deployment_target_flag('iOS', "10.1"),
+                         '-mios-version-min=10.1')
+        self.assertEqual(tools.apple_deployment_target_flag('watchOS', "10.1"),
+                         '-mwatchos-version-min=10.1')
+        self.assertEqual(tools.apple_deployment_target_flag('tvOS', "10.1"),
+                         '-mappletvos-version-min=10.1')
+        self.assertEqual('', tools.apple_deployment_target_flag('Solaris', "10.1"))
+
+    @unittest.skipUnless(platform.system() == "Darwin", "Requires OSX")
+    def test_xcrun(self):
+
+        def _common_asserts(xcrun_):
+            self.assertTrue(xcrun_.cc.endswith('clang'))
+            self.assertTrue(xcrun_.cxx.endswith('clang++'))
+            self.assertTrue(xcrun_.ar.endswith('ar'))
+            self.assertTrue(xcrun_.ranlib.endswith('ranlib'))
+            self.assertTrue(xcrun_.strip.endswith('strip'))
+            self.assertTrue(xcrun_.find('lipo').endswith('lipo'))
+            self.assertTrue(os.path.isdir(xcrun_.sdk_path))
+
+        settings = FakeSettings('Macos', 'x86')
+        xcrun = tools.XCRun(settings)
+        _common_asserts(xcrun)
+
+        settings = FakeSettings('iOS', 'x86')
+        xcrun = tools.XCRun(settings, sdk='macosx')
+        _common_asserts(xcrun)
+        # Simulator
+        self.assertNotIn("iPhoneOS", xcrun.sdk_path)
+
+        settings = FakeSettings('iOS', 'armv7')
+        xcrun = tools.XCRun(settings)
+        _common_asserts(xcrun)
+        self.assertIn("iPhoneOS", xcrun.sdk_path)
+
+        settings = FakeSettings('watchOS', 'armv7')
+        xcrun = tools.XCRun(settings)
+        _common_asserts(xcrun)
+        self.assertIn("WatchOS", xcrun.sdk_path)
+
+        # Default one
+        settings = FakeSettings(None, None)
+        xcrun = tools.XCRun(settings)
+        _common_asserts(xcrun)