diff --git a/recipes/README.recipes.md b/recipes/README.recipes.md
index 7b3d3a716..2ba15a6a4 100644
--- a/recipes/README.recipes.md
+++ b/recipes/README.recipes.md
@@ -721,7 +721,7 @@ Args:
— **def [upload](/recipes/recipe_modules/gsutil/api.py#98)(self, source, bucket, dest, args=None, link_name='gsutil.upload', metadata=None, unauthenticated_url=False, \*\*kwargs):**
### *recipe_modules* / [osx\_sdk](/recipes/recipe_modules/osx_sdk)
-[DEPS](/recipes/recipe_modules/osx_sdk/__init__.py#7): [recipe\_engine/cipd][recipe_engine/recipe_modules/cipd], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/raw\_io][recipe_engine/recipe_modules/raw_io], [recipe\_engine/step][recipe_engine/recipe_modules/step], [recipe\_engine/version][recipe_engine/recipe_modules/version]
+[DEPS](/recipes/recipe_modules/osx_sdk/__init__.py#7): [recipe\_engine/cipd][recipe_engine/recipe_modules/cipd], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/raw\_io][recipe_engine/recipe_modules/raw_io], [recipe\_engine/step][recipe_engine/recipe_modules/step], [recipe\_engine/version][recipe_engine/recipe_modules/version]
The `osx_sdk` module provides safe functions to access a semi-hermetic
@@ -729,11 +729,11 @@ XCode installation.
Available only to Google-run bots.
-#### **class [OSXSDKApi](/recipes/recipe_modules/osx_sdk/api.py#40)([RecipeApi][recipe_engine/wkt/RecipeApi]):**
+#### **class [OSXSDKApi](/recipes/recipe_modules/osx_sdk/api.py#44)([RecipeApi][recipe_engine/wkt/RecipeApi]):**
API for using OS X SDK distributed via CIPD.
- **@contextmanager**
— **def [\_\_call\_\_](/recipes/recipe_modules/osx_sdk/api.py#58)(self, kind):**
+ **@contextmanager**
— **def [\_\_call\_\_](/recipes/recipe_modules/osx_sdk/api.py#62)(self, kind):**
Sets up the XCode SDK environment.
@@ -781,7 +781,7 @@ Args:
Raises:
StepFailure or InfraFailure.
-— **def [initialize](/recipes/recipe_modules/osx_sdk/api.py#51)(self):**
+— **def [initialize](/recipes/recipe_modules/osx_sdk/api.py#55)(self):**
### *recipe_modules* / [presubmit](/recipes/recipe_modules/presubmit)
[DEPS](/recipes/recipe_modules/presubmit/__init__.py#13): [bot\_update](#recipe_modules-bot_update), [depot\_tools](#recipe_modules-depot_tools), [gclient](#recipe_modules-gclient), [git](#recipe_modules-git), [tryserver](#recipe_modules-tryserver), [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/cq][recipe_engine/recipe_modules/cq], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/resultdb][recipe_engine/recipe_modules/resultdb], [recipe\_engine/step][recipe_engine/recipe_modules/step]
diff --git a/recipes/recipe_modules/osx_sdk/__init__.py b/recipes/recipe_modules/osx_sdk/__init__.py
index b6920b4b4..57d44e6d2 100644
--- a/recipes/recipe_modules/osx_sdk/__init__.py
+++ b/recipes/recipe_modules/osx_sdk/__init__.py
@@ -10,6 +10,7 @@ DEPS = [
'recipe_engine/json',
'recipe_engine/path',
'recipe_engine/platform',
+ 'recipe_engine/properties',
'recipe_engine/raw_io',
'recipe_engine/step',
'recipe_engine/version',
diff --git a/recipes/recipe_modules/osx_sdk/api.py b/recipes/recipe_modules/osx_sdk/api.py
index 053bff6ea..b7daed0ad 100644
--- a/recipes/recipe_modules/osx_sdk/api.py
+++ b/recipes/recipe_modules/osx_sdk/api.py
@@ -22,6 +22,10 @@ _PROPERTY_DEFAULTS = {
#
# 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/xcode_binaries/mac-amd64
+#
# Keep this sorted by OS version.
_DEFAULT_VERSION_MAP = [
('10.12.6', '9c40b'),
@@ -131,8 +135,8 @@ class OSXSDKApi(recipe_api.RecipeApi):
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('14.4')))
+ 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):
diff --git a/recipes/recipe_modules/osx_sdk/examples/full.expected/ancient_version.json b/recipes/recipe_modules/osx_sdk/examples/full.expected/ancient_version.json
index da94fdf10..e9e7e1fcf 100644
--- a/recipes/recipe_modules/osx_sdk/examples/full.expected/ancient_version.json
+++ b/recipes/recipe_modules/osx_sdk/examples/full.expected/ancient_version.json
@@ -36,7 +36,7 @@
"infra_step": true,
"name": "find macOS version",
"~followup_annotations": [
- "@@@STEP_TEXT@Running on '10.1.0'.@@@"
+ "@@@STEP_TEXT@Running on '10.1'.@@@"
]
},
{
diff --git a/recipes/recipe_modules/osx_sdk/examples/full.py b/recipes/recipe_modules/osx_sdk/examples/full.py
index 04388820e..b0bc92dde 100644
--- a/recipes/recipe_modules/osx_sdk/examples/full.py
+++ b/recipes/recipe_modules/osx_sdk/examples/full.py
@@ -27,21 +27,29 @@ def GenTests(api):
yield api.test(
'explicit_version',
api.platform.name('mac'),
- api.properties(**{'$depot_tools/osx_sdk': {
- 'sdk_version': 'deadbeef',
- }})
+ api.osx_sdk.pick_sdk_version('deadbeef'),
)
yield api.test(
'automatic_version',
api.platform.name('mac'),
- api.step_data('find macOS version',
- stdout=api.raw_io.output_text('10.15.6')),
+ api.osx_sdk.macos_version('10.15.6'),
)
yield api.test(
'ancient_version',
api.platform.name('mac'),
- api.step_data('find macOS version',
- stdout=api.raw_io.output_text('10.1.0')),
+ api.osx_sdk.macos_version('10.1'),
)
+
+ bad_versions = [
+ 'meep.morp',
+ '1.2.3.4',
+ '10',
+ ]
+ for version in bad_versions:
+ try:
+ api.osx_sdk.macos_version(version)
+ assert False, f'macos_version regex failure, allowed {version=}' # pragma: no cover
+ except ValueError:
+ pass
diff --git a/recipes/recipe_modules/osx_sdk/test_api.py b/recipes/recipe_modules/osx_sdk/test_api.py
new file mode 100644
index 000000000..37dd91bf6
--- /dev/null
+++ b/recipes/recipe_modules/osx_sdk/test_api.py
@@ -0,0 +1,59 @@
+# Copyright 2024 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import re
+
+from recipe_engine import recipe_test_api
+
+
+class OSXSDKTestApi(recipe_test_api.RecipeTestApi):
+ # In tests, this will be the version that we simulate macOS to be.
+ DEFAULT_MACOS_VERSION = '14.4'
+
+ def macos_version(
+ self,
+ major_minor: str = DEFAULT_MACOS_VERSION) -> recipe_test_api.TestData:
+ """Mock the macOS Major.Minor[.Patch] version that osx_sdk will use to pick
+ the Xcode SDK version from its internal table.
+
+ This will only be used if the recipe does not explicitly select an SDK
+ version via the osx_sdk properties (which can be mocked via the
+ `pick_sdk_version` method below).
+
+ Example use:
+
+ yield api.test(
+ 'my-test-name',
+ api.osx_sdk.macos_version('13.3'),
+ )
+ """
+ if not re.match(r'^\d+(\.\d+){1,2}$', major_minor):
+ raise ValueError(
+ f'Expected Major.Minor[.Patch] (e.g. 14.4), got {major_minor=}')
+
+ return self.step_data('find macOS version',
+ stdout=self.m.raw_io.output_text(major_minor))
+
+ def pick_sdk_version(self,
+ sdk_version: str = 'deadbeef'
+ ) -> recipe_test_api.TestData:
+ """This should be used to pick a precise SDK version.
+
+ Recipes used on builders which configure the XCode version via properties
+ should use this to more accurately reflect how these recipes will run in
+ production. Specifically, when the XCode version is selected via properties,
+ the osx_sdk module will not need or attempt to discover the current macOS
+ version (which is mockable with the `macos_version` method above).
+
+ Example use:
+
+ yield api.test(
+ 'my-test-name',
+ api.osx_sdk.pick_sdk_version('13c100'),
+ )
+ """
+ return self.m.properties(
+ **{"$depot_tools/osx_sdk": {
+ "sdk_version": sdk_version,
+ }})