Support devicelab flag in osx_sdk module

This is part of osx_sdk and devicelab_osx_sdk consolidation.
This is a no-op for all targets.

LED: https://ci.chromium.org/raw/build/logs.chromium.org/flutter/led/keyonghan_google.com/46740c504ffa02ca737a1dd966a432e5882f3bddb54eb6297759436b082c30e3/+/build.proto?server=chromium-swarm.appspot.com

Change-Id: I6509006b83507e4a96c2fa2168f39485ab2f7d16
Bug: https://github.com/flutter/flutter/issues/117541
Reviewed-on: https://flutter-review.googlesource.com/c/recipes/+/43824
Reviewed-by: Godofredo Contreras <godofredoc@google.com>
Commit-Queue: Keyong Han <keyonghan@google.com>
(cherry picked from commit c6405e9ca4e3bd8be3ef1bb447e6020eab4ec488)
Reviewed-on: https://flutter-review.googlesource.com/c/recipes/+/44725
diff --git a/recipe_modules/osx_sdk/api.py b/recipe_modules/osx_sdk/api.py
index fe22bc0..bde8d00 100644
--- a/recipe_modules/osx_sdk/api.py
+++ b/recipe_modules/osx_sdk/api.py
@@ -74,7 +74,7 @@
         self._sdk_version = _DEFAULT_VERSION_MAP[0][-1]
 
   @contextmanager
-  def __call__(self, kind):
+  def __call__(self, kind, devicelab=False):
     """Sets up the XCode SDK environment.
 
     Is a no-op on non-mac platforms.
@@ -111,6 +111,8 @@
       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).
+      devicelab (bool): whether this is a devicelab tasks. The xcode for devicelab
+        is installed in a fixed location `/opt/flutter/xcode`.
 
     Raises:
         StepFailure or InfraFailure.
@@ -122,8 +124,9 @@
 
     try:
       with self.m.context(infra_steps=True):
-        self._clean_cache()
-        app = self._ensure_sdk(kind)
+        app = None
+        self._clean_cache(devicelab=devicelab)
+        app = self._ensure_sdk(kind, devicelab=devicelab)
         self.m.os_utils.kill_simulators()
         self.m.step('select XCode', ['sudo', 'xcode-select', '--switch', app])
         self.m.step('list simulators', ['xcrun', 'simctl', 'list'])
@@ -132,41 +135,58 @@
       with self.m.context(infra_steps=True):
         self.m.step('reset XCode', ['sudo', 'xcode-select', '--reset'])
 
-  def _clean_cache(self):
+  def _clean_cache(self, devicelab=False):
     """Cleans up cache when specified or polluted.
 
     Cleans up only corresponding versioned xcode instead of the whole `osx_sdk`.
     """
-    if self._cleanup_cache or self._cache_polluted():
-      self.m.file.rmtree('Cleaning up Xcode cache', self._cache_dir())
+    if self._cleanup_cache or self._cache_polluted(devicelab=devicelab):
+      self.m.file.rmtree(
+          'Cleaning up Xcode cache', self._xcode_dir(devicelab=devicelab)
+      )
 
-  def _ensure_sdk(self, kind):
-    """Ensures the mac_toolchain tool and OSX SDK packages are installed.
-
-    Returns Path to the installed sdk app bundle."""
-    cache_dir = self._cache_dir()
+  def _ensure_mac_toolchain(self, tool_dir):
     ef = self.m.cipd.EnsureFile()
     ef.add_package(self._tool_pkg, self._tool_ver)
-    self.m.cipd.ensure(cache_dir, ef)
+    self.m.cipd.ensure(tool_dir, ef)
 
-    sdk_app = cache_dir.join('XCode.app')
+  def _install_xcode(self, tool_dir, kind, app_dir):
+    """Installs xcode using mac_toolchain."""
     self.m.step(
         'install xcode',
         [
-            cache_dir.join('mac_toolchain'), 'install', '-kind', kind,
-            '-xcode-version', self._sdk_version, '-output-dir', sdk_app,
+            tool_dir.join('mac_toolchain'), 'install', '-kind', kind,
+            '-xcode-version', self._sdk_version, '-output-dir', app_dir,
             '-cipd-package-prefix', 'flutter_internal/ios/xcode',
             '-with-runtime=%s' % (not bool(self._runtime_versions))
         ],
     )
+
+  def _ensure_sdk(self, kind, devicelab=False):
+    """Ensures the mac_toolchain tool and OSX SDK packages are installed.
+
+    Returns Path to the installed sdk app bundle."""
+    app_dir = self._xcode_dir(devicelab=devicelab)
+    tool_dir = self.m.path.mkdtemp().join('osx_sdk') if devicelab else app_dir
+    self._ensure_mac_toolchain(tool_dir)
+    sdk_app = self.m.path.join(app_dir, 'XCode.app')
+    self._install_xcode(tool_dir, kind, sdk_app)
+
+    if devicelab:
+      return sdk_app
+
     # Skips runtime installation if it already exists. Otherwise,
     # installs each runtime version under `osx_sdk` for cache sharing,
     # and then copies over to the destination.
     if self._runtime_versions:
-      self.m.file.ensure_directory('Ensuring runtimes directory', sdk_app.join(*_RUNTIMESPATH))
+      self.m.file.ensure_directory(
+          'Ensuring runtimes directory',
+          self.m.path.join(sdk_app, *_RUNTIMESPATH)
+      )
       for version in self._runtime_versions:
-        runtime_name = 'iOS %s.simruntime' % version.lower().replace('ios-', '').replace('-', '.')
-        dest = sdk_app.join(*_RUNTIMESPATH).join(runtime_name)
+        runtime_name = 'iOS %s.simruntime' % version.lower(
+        ).replace('ios-', '').replace('-', '.')
+        dest = self.m.path.join(sdk_app, *_RUNTIMESPATH, runtime_name)
         if not self.m.path.exists(dest):
           runtime_cache_dir = self.m.path['cache'].join(_XCODE_CACHE_PATH).join(
               'xcode_runtime_%s' % version.lower()
@@ -174,7 +194,7 @@
           self.m.step(
               'install xcode runtime %s' % version.lower(),
               [
-                  cache_dir.join('mac_toolchain'),
+                  app_dir.join('mac_toolchain'),
                   'install-runtime',
                   '-cipd-package-prefix',
                   'flutter_internal/ios/xcode',
@@ -192,7 +212,7 @@
           self.m.file.copytree('Copy runtime to %s' % dest, source, dest, symlinks=True)
     return sdk_app
 
-  def _cache_polluted(self):
+  def _cache_polluted(self, devicelab=False):
     """Checks if cache is polluted.
 
     CIPD ensures package whenever called, but just checks on some levels, like
@@ -209,24 +229,28 @@
     able to detect some incomplete installation cases and reinstall.
     """
     cache_polluted = False
-    sdk_app_dir = self._cache_dir()
+    sdk_app_dir = self._xcode_dir(devicelab=devicelab)
     if not self.m.path.exists(sdk_app_dir):
       self.m.step('xcode not installed', ['echo', sdk_app_dir])
       return cache_polluted
-    if not self._runtime_exists():
+    if not self._runtime_exists(devicelab=devicelab):
       cache_polluted = True
       self.m.step('cache polluted due to missing runtime', ['echo', 'xcode is installed without runtime'])
     return cache_polluted
 
-  def _cache_dir(self):
+  def _xcode_dir(self, devicelab=False):
     """Returns xcode cache dir.
 
-    For an xcode without runtime, the path looks like
-        xcode_<xcode-version>
+    For a devicelab task, the path is prefixed at `/opt/flutter/xcode`.
 
-    For an xcode with runtimes, the path looks like
+    For a host only task without runtime, the path looks like
+            xcode_<xcode-version>
+
+    a host only task with runtimes, the path looks like
         xcode_<xcode-version>_runtime1_<runtime1-version>_..._runtimeN_<runtimeN-version>
     """
+    if devicelab:
+      return '/opt/flutter/xcode/%s' % self._sdk_version
     runtime_version = None
     sdk_version = 'xcode_' + self._sdk_version
     if self._runtime_versions:
@@ -234,24 +258,31 @@
       sdk_version = sdk_version + '_runtime_' + runtime_version
     return self.m.path['cache'].join(_XCODE_CACHE_PATH).join(sdk_version)
 
-  def _runtime_exists(self):
+  def _runtime_exists(self, devicelab=False):
     """Checks runtime existence in the installed xcode.
 
     Checks `iOS.simruntime` for default runtime.
     Checks each specific runtime version for specified ones.
     """
     runtime_exists = True
-    sdk_app_dir = self._cache_dir().join('XCode.app')
+    sdk_app_dir = self.m.path.join(
+        self._xcode_dir(devicelab=devicelab), 'XCode.app'
+    )
     if self._runtime_versions:
       for version in self._runtime_versions:
-        runtime_name = 'iOS %s.simruntime' % version.lower().replace('ios-', '').replace('-', '.')
-        runtime_path = sdk_app_dir.join(*_RUNTIMESPATH).join(runtime_name)
+        runtime_name = 'iOS %s.simruntime' % version.lower(
+        ).replace('ios-', '').replace('-', '.')
+        runtime_path = self.m.path.join(
+            sdk_app_dir, *_RUNTIMESPATH, runtime_name
+        )
         if not self.m.path.exists(runtime_path):
           runtime_exists = False
           self.m.step('runtime: %s does not exist' % runtime_name, ['echo', runtime_path])
           break
     else:
-      runtime_path = sdk_app_dir.join(*_RUNTIMESPATH).join('iOS.simruntime')
+      runtime_path = self.m.path.join(
+          sdk_app_dir, *_RUNTIMESPATH, 'iOS.simruntime'
+      )
       if not self.m.path.exists(runtime_path):
         runtime_exists = False
         self.m.step('iOS.simruntime does not exists', ['echo', runtime_path])
diff --git a/recipe_modules/osx_sdk/examples/full.expected/ancient_version.json b/recipe_modules/osx_sdk/examples/full.expected/ancient_version.json
index 82b44ac..2b733e6 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/ancient_version.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/ancient_version.json
@@ -107,6 +107,113 @@
     "name": "reset XCode"
   },
   {
+    "cmd": [
+      "echo",
+      "/opt/flutter/xcode/9c40b"
+    ],
+    "infra_step": true,
+    "name": "xcode not installed (2)"
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CLEANUP]/tmp_tmp_1/osx_sdk",
+      "-ensure-file",
+      "infra/tools/mac_toolchain/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "ensure_installed (2)",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/mac_toolchain/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CLEANUP]/tmp_tmp_1/osx_sdk/mac_toolchain",
+      "install",
+      "-kind",
+      "mac",
+      "-xcode-version",
+      "9c40b",
+      "-output-dir",
+      "/opt/flutter/xcode/9c40b/XCode.app",
+      "-cipd-package-prefix",
+      "flutter_internal/ios/xcode",
+      "-with-runtime=True"
+    ],
+    "infra_step": true,
+    "name": "install xcode (2)"
+  },
+  {
+    "cmd": [
+      "killall",
+      "-9",
+      "com.apple.CoreSimulator.CoreSimulatorDevice"
+    ],
+    "infra_step": true,
+    "name": "kill dart (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--switch",
+      "/opt/flutter/xcode/9c40b/XCode.app"
+    ],
+    "infra_step": true,
+    "name": "select XCode (2)"
+  },
+  {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list"
+    ],
+    "infra_step": true,
+    "name": "list simulators (2)"
+  },
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn (2)"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--reset"
+    ],
+    "infra_step": true,
+    "name": "reset XCode (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/osx_sdk/examples/full.expected/automatic_version.json b/recipe_modules/osx_sdk/examples/full.expected/automatic_version.json
index 2648907..ef1dc04 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/automatic_version.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/automatic_version.json
@@ -107,6 +107,113 @@
     "name": "reset XCode"
   },
   {
+    "cmd": [
+      "echo",
+      "/opt/flutter/xcode/12a7209"
+    ],
+    "infra_step": true,
+    "name": "xcode not installed (2)"
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CLEANUP]/tmp_tmp_1/osx_sdk",
+      "-ensure-file",
+      "infra/tools/mac_toolchain/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "ensure_installed (2)",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/mac_toolchain/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CLEANUP]/tmp_tmp_1/osx_sdk/mac_toolchain",
+      "install",
+      "-kind",
+      "mac",
+      "-xcode-version",
+      "12a7209",
+      "-output-dir",
+      "/opt/flutter/xcode/12a7209/XCode.app",
+      "-cipd-package-prefix",
+      "flutter_internal/ios/xcode",
+      "-with-runtime=True"
+    ],
+    "infra_step": true,
+    "name": "install xcode (2)"
+  },
+  {
+    "cmd": [
+      "killall",
+      "-9",
+      "com.apple.CoreSimulator.CoreSimulatorDevice"
+    ],
+    "infra_step": true,
+    "name": "kill dart (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--switch",
+      "/opt/flutter/xcode/12a7209/XCode.app"
+    ],
+    "infra_step": true,
+    "name": "select XCode (2)"
+  },
+  {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list"
+    ],
+    "infra_step": true,
+    "name": "list simulators (2)"
+  },
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn (2)"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--reset"
+    ],
+    "infra_step": true,
+    "name": "reset XCode (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/osx_sdk/examples/full.expected/explicit_runtime_version.json b/recipe_modules/osx_sdk/examples/full.expected/explicit_runtime_version.json
index 193d491..6d05191 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/explicit_runtime_version.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/explicit_runtime_version.json
@@ -201,6 +201,113 @@
     "name": "reset XCode"
   },
   {
+    "cmd": [
+      "echo",
+      "/opt/flutter/xcode/deadbeef"
+    ],
+    "infra_step": true,
+    "name": "xcode not installed"
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CLEANUP]/tmp_tmp_1/osx_sdk",
+      "-ensure-file",
+      "infra/tools/mac_toolchain/${platform} 123abc",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "ensure_installed (2)",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-123abc----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/mac_toolchain/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CLEANUP]/tmp_tmp_1/osx_sdk/mac_toolchain",
+      "install",
+      "-kind",
+      "mac",
+      "-xcode-version",
+      "deadbeef",
+      "-output-dir",
+      "/opt/flutter/xcode/deadbeef/XCode.app",
+      "-cipd-package-prefix",
+      "flutter_internal/ios/xcode",
+      "-with-runtime=False"
+    ],
+    "infra_step": true,
+    "name": "install xcode (2)"
+  },
+  {
+    "cmd": [
+      "killall",
+      "-9",
+      "com.apple.CoreSimulator.CoreSimulatorDevice"
+    ],
+    "infra_step": true,
+    "name": "kill dart (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--switch",
+      "/opt/flutter/xcode/deadbeef/XCode.app"
+    ],
+    "infra_step": true,
+    "name": "select XCode (2)"
+  },
+  {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list"
+    ],
+    "infra_step": true,
+    "name": "list simulators (2)"
+  },
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn (2)"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--reset"
+    ],
+    "infra_step": true,
+    "name": "reset XCode (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/osx_sdk/examples/full.expected/explicit_version.json b/recipe_modules/osx_sdk/examples/full.expected/explicit_version.json
index 8c52cf8..7b96b55 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/explicit_version.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/explicit_version.json
@@ -112,6 +112,118 @@
     "name": "reset XCode"
   },
   {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "rmtree",
+      "/opt/flutter/xcode/deadbeef"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up Xcode cache (2)"
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CLEANUP]/tmp_tmp_1/osx_sdk",
+      "-ensure-file",
+      "infra/tools/mac_toolchain/${platform} 123abc",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "ensure_installed (2)",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-123abc----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/mac_toolchain/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CLEANUP]/tmp_tmp_1/osx_sdk/mac_toolchain",
+      "install",
+      "-kind",
+      "mac",
+      "-xcode-version",
+      "deadbeef",
+      "-output-dir",
+      "/opt/flutter/xcode/deadbeef/XCode.app",
+      "-cipd-package-prefix",
+      "flutter_internal/ios/xcode",
+      "-with-runtime=True"
+    ],
+    "infra_step": true,
+    "name": "install xcode (2)"
+  },
+  {
+    "cmd": [
+      "killall",
+      "-9",
+      "com.apple.CoreSimulator.CoreSimulatorDevice"
+    ],
+    "infra_step": true,
+    "name": "kill dart (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--switch",
+      "/opt/flutter/xcode/deadbeef/XCode.app"
+    ],
+    "infra_step": true,
+    "name": "select XCode (2)"
+  },
+  {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list"
+    ],
+    "infra_step": true,
+    "name": "list simulators (2)"
+  },
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn (2)"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--reset"
+    ],
+    "infra_step": true,
+    "name": "reset XCode (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/osx_sdk/examples/full.expected/linux.json b/recipe_modules/osx_sdk/examples/full.expected/linux.json
index 5b0f352..7f8a512 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/linux.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/linux.json
@@ -16,6 +16,22 @@
     "name": "ninja"
   },
   {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn (2)"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/osx_sdk/examples/full.expected/mac.json b/recipe_modules/osx_sdk/examples/full.expected/mac.json
index 58a24d1..ac4ad62 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/mac.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/mac.json
@@ -107,6 +107,113 @@
     "name": "reset XCode"
   },
   {
+    "cmd": [
+      "echo",
+      "/opt/flutter/xcode/9f2000"
+    ],
+    "infra_step": true,
+    "name": "xcode not installed (2)"
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CLEANUP]/tmp_tmp_1/osx_sdk",
+      "-ensure-file",
+      "infra/tools/mac_toolchain/${platform} latest",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "ensure_installed (2)",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-latest----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/mac_toolchain/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CLEANUP]/tmp_tmp_1/osx_sdk/mac_toolchain",
+      "install",
+      "-kind",
+      "mac",
+      "-xcode-version",
+      "9f2000",
+      "-output-dir",
+      "/opt/flutter/xcode/9f2000/XCode.app",
+      "-cipd-package-prefix",
+      "flutter_internal/ios/xcode",
+      "-with-runtime=True"
+    ],
+    "infra_step": true,
+    "name": "install xcode (2)"
+  },
+  {
+    "cmd": [
+      "killall",
+      "-9",
+      "com.apple.CoreSimulator.CoreSimulatorDevice"
+    ],
+    "infra_step": true,
+    "name": "kill dart (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--switch",
+      "/opt/flutter/xcode/9f2000/XCode.app"
+    ],
+    "infra_step": true,
+    "name": "select XCode (2)"
+  },
+  {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list"
+    ],
+    "infra_step": true,
+    "name": "list simulators (2)"
+  },
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn (2)"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--reset"
+    ],
+    "infra_step": true,
+    "name": "reset XCode (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/osx_sdk/examples/full.expected/no_runtime_version.json b/recipe_modules/osx_sdk/examples/full.expected/no_runtime_version.json
index 0d4547e..a945fc9 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/no_runtime_version.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/no_runtime_version.json
@@ -128,6 +128,113 @@
     "name": "reset XCode"
   },
   {
+    "cmd": [
+      "echo",
+      "/opt/flutter/xcode/deadbeef"
+    ],
+    "infra_step": true,
+    "name": "xcode not installed"
+  },
+  {
+    "cmd": [
+      "cipd",
+      "ensure",
+      "-root",
+      "[CLEANUP]/tmp_tmp_1/osx_sdk",
+      "-ensure-file",
+      "infra/tools/mac_toolchain/${platform} 123abc",
+      "-max-threads",
+      "0",
+      "-json-output",
+      "/path/to/tmp/json"
+    ],
+    "infra_step": true,
+    "name": "ensure_installed (2)",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"result\": {@@@",
+      "@@@STEP_LOG_LINE@json.output@    \"\": [@@@",
+      "@@@STEP_LOG_LINE@json.output@      {@@@",
+      "@@@STEP_LOG_LINE@json.output@        \"instance_id\": \"resolved-instance_id-of-123abc----------\", @@@",
+      "@@@STEP_LOG_LINE@json.output@        \"package\": \"infra/tools/mac_toolchain/resolved-platform\"@@@",
+      "@@@STEP_LOG_LINE@json.output@      }@@@",
+      "@@@STEP_LOG_LINE@json.output@    ]@@@",
+      "@@@STEP_LOG_LINE@json.output@  }@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "[CLEANUP]/tmp_tmp_1/osx_sdk/mac_toolchain",
+      "install",
+      "-kind",
+      "mac",
+      "-xcode-version",
+      "deadbeef",
+      "-output-dir",
+      "/opt/flutter/xcode/deadbeef/XCode.app",
+      "-cipd-package-prefix",
+      "flutter_internal/ios/xcode",
+      "-with-runtime=True"
+    ],
+    "infra_step": true,
+    "name": "install xcode (2)"
+  },
+  {
+    "cmd": [
+      "killall",
+      "-9",
+      "com.apple.CoreSimulator.CoreSimulatorDevice"
+    ],
+    "infra_step": true,
+    "name": "kill dart (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--switch",
+      "/opt/flutter/xcode/deadbeef/XCode.app"
+    ],
+    "infra_step": true,
+    "name": "select XCode (2)"
+  },
+  {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list"
+    ],
+    "infra_step": true,
+    "name": "list simulators (2)"
+  },
+  {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn (2)"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja (2)"
+  },
+  {
+    "cmd": [
+      "sudo",
+      "xcode-select",
+      "--reset"
+    ],
+    "infra_step": true,
+    "name": "reset XCode (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/osx_sdk/examples/full.expected/win.json b/recipe_modules/osx_sdk/examples/full.expected/win.json
index 5b0f352..7f8a512 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/win.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/win.json
@@ -16,6 +16,22 @@
     "name": "ninja"
   },
   {
+    "cmd": [
+      "gn",
+      "gen",
+      "out/Release"
+    ],
+    "name": "gn (2)"
+  },
+  {
+    "cmd": [
+      "ninja",
+      "-C",
+      "out/Release"
+    ],
+    "name": "ninja (2)"
+  },
+  {
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/osx_sdk/examples/full.py b/recipe_modules/osx_sdk/examples/full.py
index 3f55bfc..d50cbcf 100644
--- a/recipe_modules/osx_sdk/examples/full.py
+++ b/recipe_modules/osx_sdk/examples/full.py
@@ -17,6 +17,9 @@
   with api.osx_sdk('mac'):
     api.step('gn', ['gn', 'gen', 'out/Release'])
     api.step('ninja', ['ninja', '-C', 'out/Release'])
+  with api.osx_sdk('mac', devicelab=True):
+    api.step('gn', ['gn', 'gen', 'out/Release'])
+    api.step('ninja', ['ninja', '-C', 'out/Release'])
 
 
 def GenTests(api):