# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""The `osx_sdk` module provides safe functions to access a semi-hermetic
XCode installation.

Available only to Google-run bots."""

from contextlib import contextmanager

from recipe_engine import recipe_api

# TODO(iannucci): replace this with something sane when PROPERTIES is
# implemented with a proto message.
_PROPERTY_DEFAULTS = {
  "toolchain_pkg": "infra/tools/mac_toolchain/${platform}",
  "toolchain_ver": "git_revision:07e67ff89ff11ed0e3071867ed6bb49319a91b05",
}

# Rationalized from https://en.wikipedia.org/wiki/Xcode.
#
# Maps from OS version to the maximum supported version of Xcode for that OS.
#
# These correspond to package instance tags for:
#
#   https://chrome-infra-packages.appspot.com/p/infra_internal/ios/xcode/
#
# Keep this sorted by OS version.
_DEFAULT_VERSION_MAP = [
  ("10.12.6", "9c40b"),
  ("10.13.2", "9f2000"),
  ("10.13.6", "10b61"),
  ("10.14.3", "10g8"),
  ("10.14.4", "11b52"),
  ("10.15.4", "12d4e"),
  ("11.3", "13c100"),
  ("13.0", "14e300c"),
  ("13.5", "15c500b"),
  ("14.0", "15e204a"),
  ("15.0", "16c5032a"),
  ("15.6", "17a400"),
  ("26.2", "17f42"),
]


class OSXSDKApi(recipe_api.RecipeApi):
  """API for using OS X SDK distributed via CIPD."""

  def __init__(self, sdk_properties, *args, **kwargs):
    super(OSXSDKApi, self).__init__(*args, **kwargs)
    self._sdk_properties = _PROPERTY_DEFAULTS.copy()
    self._sdk_properties.update(sdk_properties)
    self._sdk_version = None
    self._tool_pkg = self._sdk_properties["toolchain_pkg"]
    self._tool_ver = self._sdk_properties["toolchain_ver"]

  def initialize(self):
    if not self.m.platform.is_mac:
      return

    if "sdk_version" in self._sdk_properties:
      self._sdk_version = self._sdk_properties["sdk_version"].lower()

  @contextmanager
  def __call__(self, kind):
    """Sets up the XCode SDK environment.

    Is a no-op on non-mac platforms.

    This will deploy the helper tool and the XCode.app bundle at
    `api.path.cache_dir / 'osx_sdk'`.

    To avoid machines rebuilding these on every run, set up a named cache in
    your cr-buildbucket.cfg file like:

        caches: {
          # Cache for mac_toolchain tool and XCode.app
          name: "osx_sdk"
          path: "osx_sdk"
        }

    If you have builders which e.g. use a non-current SDK, you can give them
    a uniqely named cache:

        caches: {
          # Cache for N-1 version mac_toolchain tool and XCode.app
          name: "osx_sdk_old"
          path: "osx_sdk"
        }

    Similarly, if you have mac and iOS builders you may want to distinguish the
    cache name by adding '_ios' to it. However, if you're sharing the same bots
    for both mac and iOS, consider having a single cache and just always
    fetching the iOS version. This will lead to lower overall disk utilization
    and should help to reduce cache thrashing.

    Usage:
      with api.osx_sdk('mac'):
        # sdk with mac build bits

      with api.osx_sdk('ios'):
        # sdk with mac+iOS build bits

    Args:
      kind ('mac'|'ios'): How the SDK should be configured. iOS includes the
        base XCode distribution, as well as the iOS simulators (which can be
        quite large).

    Raises:
        StepFailure or InfraFailure.
    """
    assert kind in ("mac", "ios"), "Invalid kind %r" % (kind,)
    if not self.m.platform.is_mac:
      yield
      return

    try:
      with self.m.context(infra_steps=True):
        app = self._ensure_sdk(kind)
        self.m.step("select XCode", ["sudo", "xcode-select", "--switch", app])
      yield
    finally:
      with self.m.context(infra_steps=True):
        self.m.step("reset XCode", ["sudo", "xcode-select", "--reset"])

  def _ensure_sdk(self, kind):
    """Ensures the mac_toolchain tool and OS X SDK packages are installed.

    Returns Path to the installed sdk app bundle."""
    cache_dir = self.m.path.cache_dir / "osx_sdk"

    ef = self.m.cipd.EnsureFile()
    ef.add_package(self._tool_pkg, self._tool_ver)
    self.m.cipd.ensure(cache_dir, ef)

    if self._sdk_version is None:
      find_os = self.m.step(
        "find macOS version",
        ["sw_vers", "-productVersion"],
        stdout=self.m.raw_io.output_text(),
        step_test_data=(
          lambda: self.m.raw_io.test_api.stream_output_text(
            self.test_api.DEFAULT_MACOS_VERSION
          )
        ),
      )
      cur_os = self.m.version.parse(find_os.stdout.strip())
      find_os.presentation.step_text = f"Running on {str(cur_os)!r}."
      for target_os, xcode in reversed(_DEFAULT_VERSION_MAP):
        if cur_os >= self.m.version.parse(target_os):
          self._sdk_version = xcode
          break
      else:
        self._sdk_version = _DEFAULT_VERSION_MAP[0][-1]

    sdk_app = cache_dir / "XCode.app"
    self.m.step(
      "install xcode",
      [
        cache_dir / "mac_toolchain",
        "install",
        "-kind",
        kind,
        "-xcode-version",
        self._sdk_version,
        "-output-dir",
        sdk_app,
      ],
    )
    return sdk_app
