From 785f5486e37e5def935d343724a9ad766b468f27 Mon Sep 17 00:00:00 2001 From: Aaron Sky Date: Sun, 9 Nov 2025 20:42:58 -0500 Subject: [PATCH 1/5] Support StoreKit Configuration setting in xcscheme Run action Signed-off-by: Aaron Sky --- docs/bazel.md | 3 +- examples/integration/Lib/BUILD | 5 ++- .../Lib/Resources/Configuration.storekit | 29 +++++++++++++ examples/integration/xcodeproj_targets.bzl | 1 + test/internal/xcschemes/fixture.storekit | 1 + .../xcschemes/info_constructors_tests.bzl | 3 ++ .../xcschemes/infos_from_json_tests.bzl | 41 +++++++++++++++++++ test/internal/xcschemes/utils.bzl | 1 + .../xcschemes/write_schemes_tests.bzl | 7 ++++ .../lib/XCScheme/src/CreateLaunchAction.swift | 24 +++++++++-- .../test/CreateLaunchActionTests.swift | 6 ++- .../Generator/CreateAutomaticSchemeInfo.swift | 1 + .../Generator/CreateCustomSchemeInfos.swift | 3 ++ .../src/Generator/CreateScheme.swift | 3 +- .../xcschemes/src/Generator/SchemeInfo.swift | 1 + .../test/CreateAutomaticSchemeInfoTests.swift | 9 ++++ .../xcschemes/test/SchemeInfo+Testing.swift | 2 + xcodeproj/internal/files/files.bzl | 33 +++++++++++++++ .../internal/templates/generator.BUILD.bazel | 1 + xcodeproj/internal/xcodeproj_rule.bzl | 5 +++ xcodeproj/internal/xcodeproj_runner.bzl | 20 +++++++++ .../internal/xcschemes/xcscheme_infos.bzl | 36 +++++++++++++++- .../internal/xcschemes/xcscheme_labels.bzl | 1 + .../xcschemes/xcschemes_execution.bzl | 1 + xcodeproj/xcodeproj.bzl | 7 ++++ xcodeproj/xcschemes.bzl | 6 +++ 26 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 examples/integration/Lib/Resources/Configuration.storekit create mode 100644 test/internal/xcschemes/fixture.storekit diff --git a/docs/bazel.md b/docs/bazel.md index 857cdff3d7..a10e43af27 100755 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -464,7 +464,7 @@ Defines the Profile action.
 xcschemes.run(args, build_targets, diagnostics, env, env_include_defaults, launch_target,
-              xcode_configuration)
+              storekit_configuration, xcode_configuration)
 
Defines the Run action. @@ -480,6 +480,7 @@ Defines the Run action. | env | Environment variables to use when running the launch target.

If set to `"inherit"`, then the environment variables will be supplied by the launch target (e.g. [`cc_binary.env`](https://bazel.build/reference/be/common-definitions#binary.env)). Otherwise, the `dict` of environment variables will be set as provided, and `None` or `{}` will result in no environment variables.

Each value of the `dict` can either be a string or a value returned by [`xcschemes.env_value`](#xcschemes.env_value). If a value is a string, it will be transformed into `xcschemes.env_value(value)`. For example,
xcschemes.run(
    env = {
        "VAR1": "value 1",
        "VAR 2": xcschemes.env_value("value2", enabled = False),
    },
)
will be transformed into:
xcschemes.run(
    env = {
        "VAR1": xcschemes.env_value("value 1"),
        "VAR 2": xcschemes.env_value("value2", enabled = False),
    },
)
| `"inherit"` | | env_include_defaults | Whether to include the rules_xcodeproj provided default Bazel environment variables (e.g. `BUILD_WORKING_DIRECTORY` and `BUILD_WORKSPACE_DIRECTORY`), in addition to any set by [`env`](#xcschemes.run-env). This does not apply to [`xcschemes.launch_path`](#xcschemes.launch_path)s. | `True` | | launch_target | The target to launch when running.

Can be `None`, a label string, a value returned by [`xcschemes.launch_target`](#xcschemes.launch_target), or a value returned by [`xcschemes.launch_path`](#xcschemes.launch_path). If a label string, `xcschemes.launch_target(label_str)` will be used. If `None`, `xcschemes.launch_target()` will be used, which means no launch target will be set (i.e. the `Executable` dropdown will be set to `None`). | `None` | +| storekit_configuration | A StoreKit configuration file for use with [StoreKit Testing](https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode).

Can be `None`, or a label string referring to a single configuration file. | `None` | | xcode_configuration | The name of the Xcode configuration to use to build the targets referenced in the Run action (i.e in the [`build_targets`](#xcschemes.run-build_targets) and [`launch_target`](#xcschemes.run-launch_target) attributes).

If not set, the value of [`xcodeproj.default_xcode_configuration`](#xcodeproj-default_xcode_configuration) is used. | `None` | diff --git a/examples/integration/Lib/BUILD b/examples/integration/Lib/BUILD index cea6e5930f..0339833f20 100644 --- a/examples/integration/Lib/BUILD +++ b/examples/integration/Lib/BUILD @@ -7,7 +7,10 @@ load("@build_bazel_rules_apple//apple:tvos.bzl", "tvos_framework") load("@build_bazel_rules_apple//apple:watchos.bzl", "watchos_framework") load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -exports_files(["README.md"]) +exports_files([ + "README.md", + "Resources/Configuration.storekit", +]) exports_files( ["Info.plist"], diff --git a/examples/integration/Lib/Resources/Configuration.storekit b/examples/integration/Lib/Resources/Configuration.storekit new file mode 100644 index 0000000000..de07ac2b89 --- /dev/null +++ b/examples/integration/Lib/Resources/Configuration.storekit @@ -0,0 +1,29 @@ +{ + "appPolicies" : { + "eula" : "", + "policies" : [ + { + "locale" : "en_US", + "policyText" : "", + "policyURL" : "" + } + ] + }, + "identifier" : "", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + + ], + "settings" : { + "_failTransactionsEnabled" : false + }, + "subscriptionGroups" : [ + + ], + "version" : { + "major" : 4, + "minor" : 0 + } +} diff --git a/examples/integration/xcodeproj_targets.bzl b/examples/integration/xcodeproj_targets.bzl index 3f13c5f7c8..af69993543 100644 --- a/examples/integration/xcodeproj_targets.bzl +++ b/examples/integration/xcodeproj_targets.bzl @@ -228,6 +228,7 @@ XCSCHEMES = [ ], ), ], + storekit_configuration = "//Lib:Resources/Configuration.storekit", ), ), ] diff --git a/test/internal/xcschemes/fixture.storekit b/test/internal/xcschemes/fixture.storekit new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/internal/xcschemes/fixture.storekit @@ -0,0 +1 @@ +{} diff --git a/test/internal/xcschemes/info_constructors_tests.bzl b/test/internal/xcschemes/info_constructors_tests.bzl index 3036f69e82..5f82e28b2e 100644 --- a/test/internal/xcschemes/info_constructors_tests.bzl +++ b/test/internal/xcschemes/info_constructors_tests.bzl @@ -589,6 +589,7 @@ def info_constructors_test_suite(name): env = None, env_include_defaults = "1", launch_target = xcscheme_infos_testable.make_launch_target(), + storekit_configuration = "", xcode_configuration = "", ), ) @@ -621,6 +622,7 @@ def info_constructors_test_suite(name): }, env_include_defaults = "0", launch_target = xcscheme_infos_testable.make_launch_target("L"), + storekit_configuration = "", xcode_configuration = "Run", ), @@ -651,6 +653,7 @@ def info_constructors_test_suite(name): launch_target = xcscheme_infos_testable.make_launch_target( id = "L", ), + storekit_configuration = "", xcode_configuration = "Run", ), ) diff --git a/test/internal/xcschemes/infos_from_json_tests.bzl b/test/internal/xcschemes/infos_from_json_tests.bzl index ae1331bc25..82480f1564 100644 --- a/test/internal/xcschemes/infos_from_json_tests.bzl +++ b/test/internal/xcschemes/infos_from_json_tests.bzl @@ -41,7 +41,9 @@ def _infos_from_json_test_impl(ctx): infos = xcscheme_infos.from_json( ctx.attr.json_str, + install_path = ctx.attr.install_path, default_xcode_configuration = ctx.attr.default_xcode_configuration, + storekit_configurations_map = ctx.attr.storekit_configurations_map, top_level_deps = _json_to_top_level_deps(ctx.attr.top_level_deps), ) @@ -62,7 +64,9 @@ infos_from_json_test = unittest.make( attrs = { # Inputs "default_xcode_configuration": attr.string(mandatory = True), + "install_path": attr.string(mandatory = True), "json_str": attr.string(mandatory = True), + "storekit_configurations_map": attr.string_dict(mandatory = True), "top_level_deps": attr.string(mandatory = True), # Expected @@ -86,7 +90,9 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration, + install_path, json_str, + storekit_configurations_map, top_level_deps, # Expected @@ -97,7 +103,9 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = default_xcode_configuration, + install_path = install_path, json_str = json_str, + storekit_configurations_map = storekit_configurations_map, top_level_deps = json.encode(top_level_deps), # Expected @@ -149,6 +157,7 @@ def infos_from_json_test_suite(name): }, } + install_path = "test/internal/InfosFromJSONTests.xcodeproj" full_args = [ "-a\nnewline", xcscheme_infos_testable.make_arg( @@ -390,7 +399,9 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "AppStore", + install_path = install_path, json_str = json.encode([]), + storekit_configurations_map = {}, top_level_deps = {}, # Expected @@ -404,6 +415,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "AppStore", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -418,6 +430,7 @@ def infos_from_json_test_suite(name): "test": None, }, ]), + storekit_configurations_map = {}, top_level_deps = {}, # Expected @@ -438,6 +451,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "custom", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -485,6 +499,7 @@ def infos_from_json_test_suite(name): "test": None, }, ]), + storekit_configurations_map = {}, top_level_deps = top_level_deps, # Expected @@ -514,6 +529,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "AppStore", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -530,6 +546,7 @@ def infos_from_json_test_suite(name): "test": None, }, ]), + storekit_configurations_map = {}, top_level_deps = top_level_deps, # Expected @@ -557,6 +574,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "AppStore", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -565,6 +583,7 @@ def infos_from_json_test_suite(name): "test": None, }, ]), + storekit_configurations_map = {}, top_level_deps = {}, # Expected @@ -581,6 +600,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "AppStore", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -594,11 +614,13 @@ def infos_from_json_test_suite(name): env = {"A": "B"}, env_include_defaults = "1", launch_target = full_launch_target, + storekit_configuration = "", xcode_configuration = "custom", ), "test": None, }, ]), + storekit_configurations_map = {}, top_level_deps = top_level_deps, # Expected @@ -626,6 +648,7 @@ def infos_from_json_test_suite(name): env = {"A": xcscheme_infos_testable.make_env("B")}, env_include_defaults = "1", launch_target = expected_full_launch_target, + storekit_configuration = "", xcode_configuration = "custom", ), ), @@ -639,6 +662,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "custom", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -681,11 +705,13 @@ def infos_from_json_test_suite(name): target_environment = "", working_directory = "", ), + storekit_configuration = "", xcode_configuration = "", ), "test": None, }, ]), + storekit_configurations_map = {}, top_level_deps = top_level_deps, # Expected @@ -744,6 +770,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "AppStore", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -781,11 +808,13 @@ def infos_from_json_test_suite(name): ], working_directory = "wd", ), + storekit_configuration = "", xcode_configuration = "custom", ), "test": None, }, ]), + storekit_configurations_map = {}, top_level_deps = top_level_deps, # Expected @@ -821,6 +850,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "AppStore", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -839,11 +869,15 @@ def infos_from_json_test_suite(name): env_include_defaults = "0", launch_target = full_launch_target, use_run_args_and_env = "0", + storekit_configuration = "//test/internal/xcschemes:fixture.storekit", xcode_configuration = "custom", ), "test": None, }, ]), + storekit_configurations_map = { + "//test/internal/xcschemes:fixture.storekit": "test/internal/xcschemes/fixture.storekit", + }, top_level_deps = top_level_deps, # Expected @@ -866,6 +900,9 @@ def infos_from_json_test_suite(name): env = expected_full_env, env_include_defaults = "0", launch_target = expected_full_launch_target, + # from the install path and two levels inside the project, up to the execution root + # /xcshareddata/xcschemes + storekit_configuration = "../../xcschemes/fixture.storekit", xcode_configuration = "custom", ), ), @@ -879,6 +916,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "custom", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -913,6 +951,7 @@ def infos_from_json_test_suite(name): ), }, ]), + storekit_configurations_map = {}, top_level_deps = top_level_deps, # Expected @@ -938,6 +977,7 @@ def infos_from_json_test_suite(name): # Inputs default_xcode_configuration = "AppStore", + install_path = install_path, json_str = json.encode([ { "name": "A scheme", @@ -1009,6 +1049,7 @@ def infos_from_json_test_suite(name): ), }, ]), + storekit_configurations_map = {}, top_level_deps = top_level_deps, # Expected diff --git a/test/internal/xcschemes/utils.bzl b/test/internal/xcschemes/utils.bzl index 0aa2fff8be..9c4a92654d 100644 --- a/test/internal/xcschemes/utils.bzl +++ b/test/internal/xcschemes/utils.bzl @@ -64,6 +64,7 @@ def _dict_to_run_info(d): env = _dict_of_dicts_to_env_infos(d["env"]), env_include_defaults = d["env_include_defaults"], launch_target = _dict_to_launch_target_info(d["launch_target"]), + storekit_configuration = d["storekit_configuration"], xcode_configuration = d["xcode_configuration"], ) diff --git a/test/internal/xcschemes/write_schemes_tests.bzl b/test/internal/xcschemes/write_schemes_tests.bzl index 35b920991a..45cbd7884f 100644 --- a/test/internal/xcschemes/write_schemes_tests.bzl +++ b/test/internal/xcschemes/write_schemes_tests.bzl @@ -552,6 +552,7 @@ def write_schemes_test_suite(name): ], working_directory = "run working dir", ), + storekit_configuration = "StoreKitConfig", xcode_configuration = "Run", ), test = xcscheme_infos_testable.make_test( @@ -1110,6 +1111,8 @@ def write_schemes_test_suite(name): "1", # - test - enableThreadPerformanceChecker "1", + # - run - storekitConfiguration + "", # - run - xcodeConfiguration "", # - run - launchTarget - isPath @@ -1236,6 +1239,8 @@ def write_schemes_test_suite(name): "1", # - test - enableThreadPerformanceChecker "1", + # - run - storekitConfiguration + "StoreKitConfig", # - run - xcodeConfiguration "Run", # - run - launchTarget - isPath @@ -1340,6 +1345,8 @@ def write_schemes_test_suite(name): "1", # - test - enableThreadPerformanceChecker "1", + # - run - storekitConfiguration + "", # - run - xcodeConfiguration "", # - run - launchTarget - isPath diff --git a/tools/generators/lib/XCScheme/src/CreateLaunchAction.swift b/tools/generators/lib/XCScheme/src/CreateLaunchAction.swift index a569187035..28ef7f7982 100644 --- a/tools/generators/lib/XCScheme/src/CreateLaunchAction.swift +++ b/tools/generators/lib/XCScheme/src/CreateLaunchAction.swift @@ -21,7 +21,8 @@ public struct CreateLaunchAction { environmentVariables: [EnvironmentVariable], postActions: [ExecutionAction], preActions: [ExecutionAction], - runnable: Runnable? + runnable: Runnable?, + storeKitConfiguration: String? ) -> String { return callable( /*buildConfiguration:*/ buildConfiguration, @@ -35,7 +36,8 @@ public struct CreateLaunchAction { /*environmentVariables:*/ environmentVariables, /*postActions:*/ postActions, /*preActions:*/ preActions, - /*runnable:*/ runnable + /*runnable:*/ runnable, + /*storeKitConfiguration:*/ storeKitConfiguration ) } } @@ -55,7 +57,8 @@ extension CreateLaunchAction { _ environmentVariables: [EnvironmentVariable], _ postActions: [ExecutionAction], _ preActions: [ExecutionAction], - _ runnable: Runnable? + _ runnable: Runnable?, + _ storeKitConfiguration: String? ) -> String public static func defaultCallable( @@ -70,7 +73,8 @@ extension CreateLaunchAction { environmentVariables: [EnvironmentVariable], postActions: [ExecutionAction], preActions: [ExecutionAction], - runnable: Runnable? + runnable: Runnable?, + storeKitConfiguration: String? ) -> String { // 3 spaces for indentation is intentional @@ -202,12 +206,24 @@ ignoresPersistentStateOnLaunch = "NO" runnableString = "" } + let storeKitConfigurationString = if let storeKitConfiguration, !storeKitConfiguration.isEmpty && storeKitConfiguration != "None" { + #""" + + + +"""# + } else { + "" + } + return #""" \#(preActions.preActionsString)\# \#(postActions.postActionsString)\# \#(runnableString)\# +\#(storeKitConfigurationString)\# \#(commandLineArguments.commandLineArgumentsString)\# \#(environmentVariables.environmentVariablesString)\# diff --git a/tools/generators/lib/XCScheme/test/CreateLaunchActionTests.swift b/tools/generators/lib/XCScheme/test/CreateLaunchActionTests.swift index 4cfb8368af..a9082f70a3 100644 --- a/tools/generators/lib/XCScheme/test/CreateLaunchActionTests.swift +++ b/tools/generators/lib/XCScheme/test/CreateLaunchActionTests.swift @@ -594,7 +594,8 @@ private func createLaunchActionWithDefaults( environmentVariables: [EnvironmentVariable] = [], postActions: [ExecutionAction] = [], preActions: [ExecutionAction] = [], - runnable: Runnable? = nil + runnable: Runnable? = nil, + storeKitConfiguration: String? = nil ) -> String { return CreateLaunchAction.defaultCallable( buildConfiguration: buildConfiguration, @@ -608,6 +609,7 @@ private func createLaunchActionWithDefaults( environmentVariables: environmentVariables, postActions: postActions, preActions: preActions, - runnable: runnable + runnable: runnable, + storeKitConfiguration: storeKitConfiguration ) } diff --git a/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfo.swift b/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfo.swift index ea1dcc5072..1b33204734 100644 --- a/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfo.swift +++ b/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfo.swift @@ -139,6 +139,7 @@ extension Generator.CreateAutomaticSchemeInfo { enableThreadPerformanceChecker: false, environmentVariables: runEnvironmentVariables, launchTarget: launchTarget, + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( diff --git a/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift b/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift index 56996b2938..50b6405067 100644 --- a/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift +++ b/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift @@ -499,6 +499,8 @@ set as: Bool.self, in: url ) + let storeKitConfiguration = + try consumeArg("run-storekit-configuration", as: String?.self, in: url) let xcodeConfiguration = try consumeArg("run-xcode-configuration", as: String?.self, in: url) @@ -545,6 +547,7 @@ set enableThreadPerformanceChecker: enableThreadPerformanceChecker, environmentVariables: environmentVariables, launchTarget: launchTarget, + storeKitConfiguration: storeKitConfiguration, xcodeConfiguration: xcodeConfiguration ) } diff --git a/tools/generators/xcschemes/src/Generator/CreateScheme.swift b/tools/generators/xcschemes/src/Generator/CreateScheme.swift index 0725e0c238..0ab1671f00 100644 --- a/tools/generators/xcschemes/src/Generator/CreateScheme.swift +++ b/tools/generators/xcschemes/src/Generator/CreateScheme.swift @@ -400,7 +400,8 @@ extension Generator.CreateScheme { preActions: launchPreActions .sorted(by: compareExecutionActions) .map(\.action), - runnable: launchRunnable + runnable: launchRunnable, + storeKitConfiguration: schemeInfo.run.storeKitConfiguration ), profileAction: createProfileAction( buildConfiguration: schemeInfo.profile.xcodeConfiguration ?? diff --git a/tools/generators/xcschemes/src/Generator/SchemeInfo.swift b/tools/generators/xcschemes/src/Generator/SchemeInfo.swift index b9b05e1714..99ab59dadc 100644 --- a/tools/generators/xcschemes/src/Generator/SchemeInfo.swift +++ b/tools/generators/xcschemes/src/Generator/SchemeInfo.swift @@ -70,6 +70,7 @@ struct SchemeInfo: Equatable { let enableThreadPerformanceChecker: Bool let environmentVariables: [EnvironmentVariable] let launchTarget: LaunchTarget? + let storeKitConfiguration: String? let xcodeConfiguration: String? } diff --git a/tools/generators/xcschemes/test/CreateAutomaticSchemeInfoTests.swift b/tools/generators/xcschemes/test/CreateAutomaticSchemeInfoTests.swift index 95aff2f5a8..b04aab718a 100644 --- a/tools/generators/xcschemes/test/CreateAutomaticSchemeInfoTests.swift +++ b/tools/generators/xcschemes/test/CreateAutomaticSchemeInfoTests.swift @@ -155,6 +155,7 @@ final class CreateAutomaticSchemeInfoTests: XCTestCase { primary: launchable, extensionHost: nil ), + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( @@ -237,6 +238,7 @@ final class CreateAutomaticSchemeInfoTests: XCTestCase { primary: launchable, extensionHost: extensionHost ), + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( @@ -315,6 +317,7 @@ final class CreateAutomaticSchemeInfoTests: XCTestCase { primary: launchable, extensionHost: nil ), + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( @@ -393,6 +396,7 @@ final class CreateAutomaticSchemeInfoTests: XCTestCase { primary: launchable, extensionHost: nil ), + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( @@ -465,6 +469,7 @@ final class CreateAutomaticSchemeInfoTests: XCTestCase { enableThreadPerformanceChecker: false, environmentVariables: baseEnvironmentVariables, launchTarget: nil, + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( @@ -533,6 +538,7 @@ final class CreateAutomaticSchemeInfoTests: XCTestCase { enableThreadPerformanceChecker: false, environmentVariables: [], launchTarget: nil, + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( @@ -604,6 +610,7 @@ final class CreateAutomaticSchemeInfoTests: XCTestCase { enableThreadPerformanceChecker: false, environmentVariables: [], launchTarget: nil, + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( @@ -676,6 +683,7 @@ final class CreateAutomaticSchemeInfoTests: XCTestCase { enableThreadPerformanceChecker: false, environmentVariables: [], launchTarget: nil, + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( @@ -743,6 +751,7 @@ final class CreateAutomaticSchemeInfoTests: XCTestCase { enableThreadPerformanceChecker: false, environmentVariables: [], launchTarget: nil, + storeKitConfiguration: nil, xcodeConfiguration: nil ), profile: .init( diff --git a/tools/generators/xcschemes/test/SchemeInfo+Testing.swift b/tools/generators/xcschemes/test/SchemeInfo+Testing.swift index 9868cd1bd9..67df19bdf3 100644 --- a/tools/generators/xcschemes/test/SchemeInfo+Testing.swift +++ b/tools/generators/xcschemes/test/SchemeInfo+Testing.swift @@ -54,6 +54,7 @@ extension SchemeInfo.Run { enableUBSanitizer: Bool = false, environmentVariables: [EnvironmentVariable] = [], launchTarget: SchemeInfo.LaunchTarget? = nil, + storeKitConfiguration: String? = nil, xcodeConfiguration: String? = nil ) -> Self { return Self( @@ -67,6 +68,7 @@ extension SchemeInfo.Run { enableThreadPerformanceChecker: enableThreadPerformanceChecker, environmentVariables: environmentVariables, launchTarget: launchTarget, + storeKitConfiguration: storeKitConfiguration, xcodeConfiguration: xcodeConfiguration ) } diff --git a/xcodeproj/internal/files/files.bzl b/xcodeproj/internal/files/files.bzl index 6041d7b3eb..89b604e3c1 100644 --- a/xcodeproj/internal/files/files.bzl +++ b/xcodeproj/internal/files/files.bzl @@ -130,6 +130,39 @@ def join_paths_ignoring_empty(*components): return "" return paths.join(*non_empty_components) +def relativize_unchecked(path, start): + """ + Prefer `paths.relativize(...)` from skylib over this function. This function + unconditionally attempts to establish a relative path between two paths, + regardless of whether or not the path is under the source. It is inherently + unsafe. + """ + segments = paths.normalize(path).split("/") + start_segments = paths.dirname(paths.normalize(start)).split("/") + if start_segments == ["."]: + start_segments = [] + start_length = len(start_segments) + + if ( + path.startswith("/") != start.startswith("/") or + len(segments) < start_length + ): + fail("Path '{}' has no ancestor in common with '{}'".format(path, start)) + + common_prefix_count = 0 + for ancestor_segment, segment in zip(start_segments, segments): + # This is the point we fork from `paths.relativize`. Instead of failing + # here, we break instead and form the path as best we can. This may + # ultimately be misguided. + if ancestor_segment != segment: + break + common_prefix_count += 1 + + parent_segments = [".."] * (start_length - common_prefix_count) + result_segments = parent_segments + segments[common_prefix_count:] + + return "/".join(result_segments) + def replace_bazel_placeholders(path): # Use Xcode set `DEVELOPER_DIR` path = path.replace("__BAZEL_XCODE_DEVELOPER_DIR__", "$(DEVELOPER_DIR)") diff --git a/xcodeproj/internal/templates/generator.BUILD.bazel b/xcodeproj/internal/templates/generator.BUILD.bazel index 300098e355..57f6154331 100644 --- a/xcodeproj/internal/templates/generator.BUILD.bazel +++ b/xcodeproj/internal/templates/generator.BUILD.bazel @@ -32,6 +32,7 @@ xcodeproj( scheme_autogeneration_mode = "%scheme_autogeneration_mode%", scheme_autogeneration_config = %scheme_autogeneration_config%, separate_index_build_output_base = %separate_index_build_output_base%, + storekit_configurations_map = %storekit_configurations_map%, tags = %tags%, target_name_mode = "%target_name_mode%", top_level_device_targets = %top_level_device_targets%, diff --git a/xcodeproj/internal/xcodeproj_rule.bzl b/xcodeproj/internal/xcodeproj_rule.bzl index bbe87196fe..fe41e39b56 100644 --- a/xcodeproj/internal/xcodeproj_rule.bzl +++ b/xcodeproj/internal/xcodeproj_rule.bzl @@ -523,6 +523,7 @@ def _write_schemes( infos, install_path, name, + storekit_configurations_map, top_level_deps, workspace_directory, xcschemes_generator, @@ -542,6 +543,8 @@ def _write_schemes( xcscheme_infos = xcscheme_infos_module.from_json( xcschemes_json, default_xcode_configuration = default_xcode_configuration, + install_path = install_path, + storekit_configurations_map = storekit_configurations_map, top_level_deps = top_level_deps, ) @@ -729,6 +732,7 @@ Are you using an `alias`? `xcodeproj.focused_targets` and \ infos = infos, install_path = install_path, name = name, + storekit_configurations_map = ctx.attr.storekit_configurations_map, top_level_deps = top_level_deps, workspace_directory = workspace_directory, xcschemes_generator = ctx.executable._xcschemes_generator, @@ -822,6 +826,7 @@ def _xcodeproj_attrs( "scheme_autogeneration_config": attr.string_list_dict(mandatory = True), "scheme_autogeneration_mode": attr.string(mandatory = True), "separate_index_build_output_base": attr.bool(mandatory = True), + "storekit_configurations_map": attr.string_dict(mandatory = True), "target_name_mode": attr.string(mandatory = True), "top_level_device_targets": attr.label_list( cfg = target_transitions.device, diff --git a/xcodeproj/internal/xcodeproj_runner.bzl b/xcodeproj/internal/xcodeproj_runner.bzl index f4e500cda7..5d1f392ecf 100644 --- a/xcodeproj/internal/xcodeproj_runner.bzl +++ b/xcodeproj/internal/xcodeproj_runner.bzl @@ -13,6 +13,22 @@ def _process_extra_flags(*, attr, content, setting, config, config_suffix): "common:{}{} {}".format(config, config_suffix, extra_flags), ) +def _resolve_storekit_configurations_map(storekit_configurations_map): + file_paths = {} + for scheme_name, target in storekit_configurations_map.items(): + if target.label in file_paths: + continue + files = target.files.to_list() + if not files: + continue + elif len(files) > 1: + fail("""\ +Scheme `{scheme_name}` declares a `storekit_configuration` on its `run` action \ +that is composed of multiple files. The `storekit_configuration` for a scheme \ +must be a single file.""".format(scheme_name = scheme_name)) + file_paths[str(target.label)] = files[0].path + return file_paths + def _serialize_nullable_string(value): if not value: return "None" @@ -185,6 +201,7 @@ def _write_generator_build_file( name, runner_label, repo, + storekit_configurations_map, template): output = actions.declare_file("{}.generator.BUILD.bazel".format(name)) @@ -220,6 +237,7 @@ def _write_generator_build_file( "%scheme_autogeneration_config%": str(attr.scheme_autogeneration_config), "%scheme_autogeneration_mode%": attr.scheme_autogeneration_mode, "%separate_index_build_output_base%": str(attr._separate_index_build_output_base[BuildSettingInfo].value), + "%storekit_configurations_map%": str(storekit_configurations_map), "%tags%": tags, "%target_name_mode%": attr.target_name_mode, "%testonly%": str(attr.testonly), @@ -403,6 +421,7 @@ def _xcodeproj_runner_impl(ctx): name = name, runner_label = runner_label, repo = repo, + storekit_configurations_map = _resolve_storekit_configurations_map(attr.storekit_configurations_map), template = build_file_template, ) @@ -469,6 +488,7 @@ xcodeproj_runner = rule( default = "auto", values = ["auto", "none", "all"], ), + "storekit_configurations_map": attr.string_keyed_label_dict(allow_files = True), "target_name_mode": attr.string( default = "auto", values = ["auto", "label"], diff --git a/xcodeproj/internal/xcschemes/xcscheme_infos.bzl b/xcodeproj/internal/xcschemes/xcscheme_infos.bzl index afe7ae7e2e..59e0a55118 100644 --- a/xcodeproj/internal/xcschemes/xcscheme_infos.bzl +++ b/xcodeproj/internal/xcschemes/xcscheme_infos.bzl @@ -1,5 +1,6 @@ """Module for parsing macro custom Xcode schemes json in the analysis phase.""" +load("@bazel_skylib//lib:paths.bzl", "paths") load( "//xcodeproj/internal:memory_efficiency.bzl", "EMPTY_LIST", @@ -7,6 +8,7 @@ load( "FALSE_ARG", "TRUE_ARG", ) +load("//xcodeproj/internal/files:files.bzl", "relativize_unchecked") # Constructors @@ -143,6 +145,7 @@ def _make_run( env = None, env_include_defaults = TRUE_ARG, launch_target = _make_launch_target(), + storekit_configuration = EMPTY_STRING, xcode_configuration = EMPTY_STRING): return struct( args = args, @@ -151,6 +154,7 @@ def _make_run( env = env, env_include_defaults = env_include_defaults, launch_target = launch_target, + storekit_configuration = storekit_configuration, xcode_configuration = xcode_configuration, ) @@ -522,6 +526,17 @@ def _pre_post_action_info_from_dicts(pre_post_actions): for pre_post_action in pre_post_actions ] +def _resolve_storekit_configuration( + label, + *, + install_path, + storekit_configurations_map): + if not label or label not in storekit_configurations_map: + return "" + path = storekit_configurations_map[label] + scheme_dir = paths.join(install_path, "xcshareddata", "xcschemes") + return relativize_unchecked(path, scheme_dir) + def _profile_info_from_dict( profile, *, @@ -581,7 +596,9 @@ def _run_info_from_dict( run, *, default_xcode_configuration, + install_path, scheme_name, + storekit_configurations_map, top_level_deps): if not run: return _make_run() @@ -619,6 +636,11 @@ def _run_info_from_dict( env = _env_infos_from_dict(run["env"]), env_include_defaults = run["env_include_defaults"], launch_target = launch_target, + storekit_configuration = _resolve_storekit_configuration( + run["storekit_configuration"], + install_path = install_path, + storekit_configurations_map = storekit_configurations_map, + ), xcode_configuration = xcode_configuration, ) @@ -729,13 +751,17 @@ def _scheme_info_from_dict( scheme, *, default_xcode_configuration, + install_path, + storekit_configurations_map, top_level_deps): name = scheme["name"] run = _run_info_from_dict( scheme["run"], default_xcode_configuration = default_xcode_configuration, + install_path = install_path, scheme_name = name, + storekit_configurations_map = storekit_configurations_map, top_level_deps = top_level_deps, ) @@ -759,11 +785,19 @@ def _scheme_info_from_dict( # API -def _from_json(json_str, *, default_xcode_configuration, top_level_deps): +def _from_json( + json_str, + *, + default_xcode_configuration, + install_path, + storekit_configurations_map, + top_level_deps): return [ _scheme_info_from_dict( scheme, default_xcode_configuration = default_xcode_configuration, + install_path = install_path, + storekit_configurations_map = storekit_configurations_map, top_level_deps = top_level_deps, ) for scheme in json.decode(json_str) diff --git a/xcodeproj/internal/xcschemes/xcscheme_labels.bzl b/xcodeproj/internal/xcschemes/xcscheme_labels.bzl index bdc4617fc4..0962fac645 100644 --- a/xcodeproj/internal/xcschemes/xcscheme_labels.bzl +++ b/xcodeproj/internal/xcschemes/xcscheme_labels.bzl @@ -111,6 +111,7 @@ def _resolve_run_labels(run): env = run.env, env_include_defaults = run.env_include_defaults, launch_target = _resolve_launch_target_labels(run.launch_target), + storekit_configuration = _resolve_label(run.storekit_configuration), xcode_configuration = run.xcode_configuration, ) diff --git a/xcodeproj/internal/xcschemes/xcschemes_execution.bzl b/xcodeproj/internal/xcschemes/xcschemes_execution.bzl index 8c2d05f1de..131cb7b49c 100644 --- a/xcodeproj/internal/xcschemes/xcschemes_execution.bzl +++ b/xcodeproj/internal/xcschemes/xcschemes_execution.bzl @@ -341,6 +341,7 @@ def _write_schemes( _add_env(info.run.env) custom_scheme_args.add(info.run.env_include_defaults) _add_diagnostics(info.run.diagnostics) + custom_scheme_args.add(info.run.storekit_configuration) custom_scheme_args.add(info.run.xcode_configuration) _add_launch_target( diff --git a/xcodeproj/xcodeproj.bzl b/xcodeproj/xcodeproj.bzl index 3901f9b831..ad60147a01 100644 --- a/xcodeproj/xcodeproj.bzl +++ b/xcodeproj/xcodeproj.bzl @@ -461,6 +461,12 @@ configuration alphabetically ("{default}"). target = bazel_labels.normalize_string(name), )) + storekit_configurations_map = { + scheme.name: scheme.run.storekit_configuration + for scheme in xcschemes + if scheme.run and scheme.run.storekit_configuration + } + xcschemes_json = json.encode( xcscheme_labels.resolve_labels(xcschemes), ) @@ -534,6 +540,7 @@ for {configuration} ({new_keys}) do not match keys of other configurations \ project_options = project_options, scheme_autogeneration_mode = scheme_autogeneration_mode, scheme_autogeneration_config = scheme_autogeneration_config, + storekit_configurations_map = storekit_configurations_map, target_name_mode = target_name_mode, testonly = testonly, top_level_device_targets = top_level_device_targets, diff --git a/xcodeproj/xcschemes.bzl b/xcodeproj/xcschemes.bzl index c43793736a..d0550bc664 100644 --- a/xcodeproj/xcschemes.bzl +++ b/xcodeproj/xcschemes.bzl @@ -212,6 +212,7 @@ def _run( env = "inherit", env_include_defaults = True, launch_target = None, + storekit_configuration = None, xcode_configuration = None): """Defines the Run action. @@ -335,6 +336,10 @@ def _run( `None`, `xcschemes.launch_target()` will be used, which means no launch target will be set (i.e. the `Executable` dropdown will be set to `None`). + storekit_configuration: A StoreKit configuration file for use with + [StoreKit Testing](https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode). + + Can be `None`, or a label string referring to a single configuration file. xcode_configuration: The name of the Xcode configuration to use to build the targets referenced in the Run action (i.e in the [`build_targets`](#xcschemes.run-build_targets) and @@ -351,6 +356,7 @@ def _run( env = env or {}, env_include_defaults = TRUE_ARG if env_include_defaults else FALSE_ARG, launch_target = launch_target, + storekit_configuration = storekit_configuration, xcode_configuration = xcode_configuration or "", ) From 2e310b41a7b65627e80193ec354a6e2eeaf4cbc9 Mon Sep 17 00:00:00 2001 From: Aaron Sky Date: Sun, 9 Nov 2025 20:53:41 -0500 Subject: [PATCH 2/5] buildifier and changelog Signed-off-by: Aaron Sky --- CHANGELOG.md | 1 + xcodeproj/internal/files/files.bzl | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6befc29ae3..54e0104ae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ END_UNRELEASED_TEMPLATE ### New * Added `--@rules_xcodeproj//xcodeproj:separate_index_build_output_base` flag to configure the generator to use a separate output base for index builds: [#3243](https://github.com/MobileNativeFoundation/rules_xcodeproj/pull/3243) +* Added support for StoreKit configuration files to `xcschemes.run`, for use with [StoreKit Testing](https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode). ### Adjusted diff --git a/xcodeproj/internal/files/files.bzl b/xcodeproj/internal/files/files.bzl index 89b604e3c1..7a8fb1ed7a 100644 --- a/xcodeproj/internal/files/files.bzl +++ b/xcodeproj/internal/files/files.bzl @@ -136,6 +136,10 @@ def relativize_unchecked(path, start): unconditionally attempts to establish a relative path between two paths, regardless of whether or not the path is under the source. It is inherently unsafe. + + Args: + path: The path to relativize, as the destination. + start: The path to relativize against, as the source. """ segments = paths.normalize(path).split("/") start_segments = paths.dirname(paths.normalize(start)).split("/") From c3f2a3fcab4a20d965b404e74432c3749db85bdc Mon Sep 17 00:00:00 2001 From: Aaron Sky Date: Sun, 9 Nov 2025 20:56:06 -0500 Subject: [PATCH 3/5] buildifier Signed-off-by: Aaron Sky --- xcodeproj/internal/files/files.bzl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xcodeproj/internal/files/files.bzl b/xcodeproj/internal/files/files.bzl index 7a8fb1ed7a..d3e63983af 100644 --- a/xcodeproj/internal/files/files.bzl +++ b/xcodeproj/internal/files/files.bzl @@ -132,6 +132,8 @@ def join_paths_ignoring_empty(*components): def relativize_unchecked(path, start): """ + Calculates a naïve relative path from a `start` path to `path`. + Prefer `paths.relativize(...)` from skylib over this function. This function unconditionally attempts to establish a relative path between two paths, regardless of whether or not the path is under the source. It is inherently @@ -140,6 +142,9 @@ def relativize_unchecked(path, start): Args: path: The path to relativize, as the destination. start: The path to relativize against, as the source. + + Returns: + A path string. """ segments = paths.normalize(path).split("/") start_segments = paths.dirname(paths.normalize(start)).split("/") From b934c3cffa986abddcdae31f10e84339fbee3fde Mon Sep 17 00:00:00 2001 From: Aaron Sky Date: Wed, 19 Nov 2025 04:24:38 -0500 Subject: [PATCH 4/5] move path relativization to the generator, be sad Signed-off-by: Aaron Sky --- .../xcschemes/infos_from_json_tests.bzl | 3 +- .../Generator/CreateCustomSchemeInfos.swift | 54 ++++++++++++++++--- .../xcschemes/src/Generator/Generator.swift | 13 ++++- xcodeproj/internal/files/files.bzl | 42 --------------- xcodeproj/internal/xcodeproj_rule.bzl | 7 ++- xcodeproj/internal/xcodeproj_runner.bzl | 10 +++- .../internal/xcschemes/xcscheme_infos.bzl | 34 +++++------- 7 files changed, 88 insertions(+), 75 deletions(-) diff --git a/test/internal/xcschemes/infos_from_json_tests.bzl b/test/internal/xcschemes/infos_from_json_tests.bzl index 82480f1564..261e4bb8a0 100644 --- a/test/internal/xcschemes/infos_from_json_tests.bzl +++ b/test/internal/xcschemes/infos_from_json_tests.bzl @@ -41,7 +41,6 @@ def _infos_from_json_test_impl(ctx): infos = xcscheme_infos.from_json( ctx.attr.json_str, - install_path = ctx.attr.install_path, default_xcode_configuration = ctx.attr.default_xcode_configuration, storekit_configurations_map = ctx.attr.storekit_configurations_map, top_level_deps = _json_to_top_level_deps(ctx.attr.top_level_deps), @@ -902,7 +901,7 @@ def infos_from_json_test_suite(name): launch_target = expected_full_launch_target, # from the install path and two levels inside the project, up to the execution root # /xcshareddata/xcschemes - storekit_configuration = "../../xcschemes/fixture.storekit", + storekit_configuration = "test/internal/xcschemes/fixture.storekit", xcode_configuration = "custom", ), ), diff --git a/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift b/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift index 50b6405067..ee070c558b 100644 --- a/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift +++ b/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift @@ -23,7 +23,9 @@ extension Generator { environmentVariables: [TargetID: [EnvironmentVariable]], executionActionsFile: URL, extensionHostIDs: [TargetID: [TargetID]], - targetsByID: [TargetID: Target] + schemesDirectory: URL, + targetsByID: [TargetID: Target], + workspace: URL ) async throws -> [SchemeInfo] { try await callable( /*commandLineArguments:*/ commandLineArguments, @@ -31,7 +33,9 @@ extension Generator { /*environmentVariables:*/ environmentVariables, /*executionActionsFile:*/ executionActionsFile, /*extensionHostIDs:*/ extensionHostIDs, - /*targetsByID:*/ targetsByID + /*schemesDirectory:*/ schemesDirectory, + /*targetsByID:*/ targetsByID, + /*workspace:*/ workspace ) } } @@ -46,7 +50,9 @@ extension Generator.CreateCustomSchemeInfos { _ environmentVariables: [TargetID: [EnvironmentVariable]], _ executionActionsFile: URL, _ extensionHostIDs: [TargetID: [TargetID]], - _ targetsByID: [TargetID: Target] + _ schemesDirectory: URL, + _ targetsByID: [TargetID: Target], + _ workspace: URL ) async throws -> [SchemeInfo] static func defaultCallable( @@ -55,7 +61,9 @@ extension Generator.CreateCustomSchemeInfos { environmentVariables: [TargetID: [EnvironmentVariable]], executionActionsFile: URL, extensionHostIDs: [TargetID: [TargetID]], - targetsByID: [TargetID: Target] + schemesDirectory: URL, + targetsByID: [TargetID: Target], + workspace: URL ) async throws -> [SchemeInfo] { let executionActions: [String: [SchemeInfo.ExecutionAction]] = try await .parse( @@ -93,9 +101,11 @@ extension Generator.CreateCustomSchemeInfos { allTargetIDs: &allTargetIDs, extensionHostIDs: extensionHostIDs, name: name, + schemesDirectory: schemesDirectory, targetCommandLineArguments: commandLineArguments, targetEnvironmentVariables: environmentVariables, - targetsByID: targetsByID + targetsByID: targetsByID, + workspace: workspace ) let profile = try rawArgs.consumeArg( @@ -450,9 +460,11 @@ set allTargetIDs: inout Set, extensionHostIDs: [TargetID: [TargetID]], name: String, + schemesDirectory: URL, targetCommandLineArguments: [TargetID: [CommandLineArgument]], targetEnvironmentVariables: [TargetID: [EnvironmentVariable]], targetsByID: [TargetID: Target], + workspace: URL, file: StaticString = #filePath, line: UInt = #line ) throws -> SchemeInfo.Run { @@ -500,7 +512,11 @@ set in: url ) let storeKitConfiguration = - try consumeArg("run-storekit-configuration", as: String?.self, in: url) + (try consumeArg("run-storekit-configuration", as: String?.self, in: url)).map { + // relativize the StoreKit Testing configuration file against the scheme directory within the install path + URL(filePath: $0, relativeTo: workspace) + .relativize(from: schemesDirectory) + } let xcodeConfiguration = try consumeArg("run-xcode-configuration", as: String?.self, in: url) @@ -805,3 +821,29 @@ private extension SchemeInfo.LaunchTarget { } } } + +private extension URL { + func relativize(from source: URL) -> String { + let sourceComponents = source.deletingLastPathComponent().pathComponents + let destComponents = self.pathComponents + + // Find common prefix + var commonPrefixCount = 0 + while commonPrefixCount < sourceComponents.count && + commonPrefixCount < destComponents.count && + sourceComponents[commonPrefixCount] == destComponents[commonPrefixCount] { + commonPrefixCount += 1 + } + + // Build relative path + var result = [String]() + + // Add "../" for each level to go up + result.append(contentsOf: Array(repeating: "..", count: sourceComponents.count - commonPrefixCount)) + + // Add remaining destination components + result.append(contentsOf: destComponents[commonPrefixCount...]) + + return result.joined(separator: "/") + } +} diff --git a/tools/generators/xcschemes/src/Generator/Generator.swift b/tools/generators/xcschemes/src/Generator/Generator.swift index b7ffd0c674..b81526184e 100644 --- a/tools/generators/xcschemes/src/Generator/Generator.swift +++ b/tools/generators/xcschemes/src/Generator/Generator.swift @@ -1,3 +1,4 @@ +import Foundation import PBXProj import ToolCommon import XCScheme @@ -39,13 +40,23 @@ struct Generator { from: arguments.autogenerationConfigFile ) + let workspace = URL(filePath: arguments.workspace, directoryHint: .isDirectory) + let installPath = if arguments.installPath.isEmpty { + workspace + } else { + URL(filePath: arguments.installPath, directoryHint: .isDirectory, relativeTo: workspace) + } + let schemesDirectory = URL(filePath: "xcshareddata/xcschemes", directoryHint: .isDirectory, relativeTo: installPath) + let customSchemeInfos = try await environment.createCustomSchemeInfos( commandLineArguments: commandLineArguments, customSchemesFile: arguments.customSchemesFile, environmentVariables: environmentVariables, executionActionsFile: arguments.executionActionsFile, extensionHostIDs: extensionHostIDs, - targetsByID: targetsByID + schemesDirectory: schemesDirectory, + targetsByID: targetsByID, + workspace: workspace ) let automaticSchemeInfos = try environment.createAutomaticSchemeInfos( diff --git a/xcodeproj/internal/files/files.bzl b/xcodeproj/internal/files/files.bzl index d3e63983af..6041d7b3eb 100644 --- a/xcodeproj/internal/files/files.bzl +++ b/xcodeproj/internal/files/files.bzl @@ -130,48 +130,6 @@ def join_paths_ignoring_empty(*components): return "" return paths.join(*non_empty_components) -def relativize_unchecked(path, start): - """ - Calculates a naïve relative path from a `start` path to `path`. - - Prefer `paths.relativize(...)` from skylib over this function. This function - unconditionally attempts to establish a relative path between two paths, - regardless of whether or not the path is under the source. It is inherently - unsafe. - - Args: - path: The path to relativize, as the destination. - start: The path to relativize against, as the source. - - Returns: - A path string. - """ - segments = paths.normalize(path).split("/") - start_segments = paths.dirname(paths.normalize(start)).split("/") - if start_segments == ["."]: - start_segments = [] - start_length = len(start_segments) - - if ( - path.startswith("/") != start.startswith("/") or - len(segments) < start_length - ): - fail("Path '{}' has no ancestor in common with '{}'".format(path, start)) - - common_prefix_count = 0 - for ancestor_segment, segment in zip(start_segments, segments): - # This is the point we fork from `paths.relativize`. Instead of failing - # here, we break instead and form the path as best we can. This may - # ultimately be misguided. - if ancestor_segment != segment: - break - common_prefix_count += 1 - - parent_segments = [".."] * (start_length - common_prefix_count) - result_segments = parent_segments + segments[common_prefix_count:] - - return "/".join(result_segments) - def replace_bazel_placeholders(path): # Use Xcode set `DEVELOPER_DIR` path = path.replace("__BAZEL_XCODE_DEVELOPER_DIR__", "$(DEVELOPER_DIR)") diff --git a/xcodeproj/internal/xcodeproj_rule.bzl b/xcodeproj/internal/xcodeproj_rule.bzl index fe41e39b56..c08fe8d23b 100644 --- a/xcodeproj/internal/xcodeproj_rule.bzl +++ b/xcodeproj/internal/xcodeproj_rule.bzl @@ -543,7 +543,6 @@ def _write_schemes( xcscheme_infos = xcscheme_infos_module.from_json( xcschemes_json, default_xcode_configuration = default_xcode_configuration, - install_path = install_path, storekit_configurations_map = storekit_configurations_map, top_level_deps = top_level_deps, ) @@ -826,7 +825,11 @@ def _xcodeproj_attrs( "scheme_autogeneration_config": attr.string_list_dict(mandatory = True), "scheme_autogeneration_mode": attr.string(mandatory = True), "separate_index_build_output_base": attr.bool(mandatory = True), - "storekit_configurations_map": attr.string_dict(mandatory = True), + "storekit_configurations_map": attr.string_dict( + mandatory = True, + doc = """\ +A dict mapping of Labels for StoreKit Testing configuration files to their File paths.""", + ), "target_name_mode": attr.string(mandatory = True), "top_level_device_targets": attr.label_list( cfg = target_transitions.device, diff --git a/xcodeproj/internal/xcodeproj_runner.bzl b/xcodeproj/internal/xcodeproj_runner.bzl index 5d1f392ecf..8ffdfda3ad 100644 --- a/xcodeproj/internal/xcodeproj_runner.bzl +++ b/xcodeproj/internal/xcodeproj_runner.bzl @@ -488,7 +488,15 @@ xcodeproj_runner = rule( default = "auto", values = ["auto", "none", "all"], ), - "storekit_configurations_map": attr.string_keyed_label_dict(allow_files = True), + "storekit_configurations_map": attr.string_keyed_label_dict( + allow_files = True, + mandatory = True, + doc = """\ +A dict mapping xcscheme names to Labels of StoreKit Testing configuration files. + +While the attr allows for Labels that make up multiple files, the Label must point +to a single file.""", + ), "target_name_mode": attr.string( default = "auto", values = ["auto", "label"], diff --git a/xcodeproj/internal/xcschemes/xcscheme_infos.bzl b/xcodeproj/internal/xcschemes/xcscheme_infos.bzl index 59e0a55118..d28e0039db 100644 --- a/xcodeproj/internal/xcschemes/xcscheme_infos.bzl +++ b/xcodeproj/internal/xcschemes/xcscheme_infos.bzl @@ -1,6 +1,5 @@ """Module for parsing macro custom Xcode schemes json in the analysis phase.""" -load("@bazel_skylib//lib:paths.bzl", "paths") load( "//xcodeproj/internal:memory_efficiency.bzl", "EMPTY_LIST", @@ -8,7 +7,6 @@ load( "FALSE_ARG", "TRUE_ARG", ) -load("//xcodeproj/internal/files:files.bzl", "relativize_unchecked") # Constructors @@ -326,6 +324,17 @@ def _env_infos_from_dict(env): for key, value in env.items() } +def _storekit_configuration_info(label, storekit_configurations_map): + """ + Extract the full path (from the execution root) for a StoreKit Testing \ + configuration file from the `storekit_configurations_map`. + + Args: + label: A Label to a StoreKit Testing configuration file. + storekit_configurations_map: A dict of Labels to File paths. + """ + return storekit_configurations_map.get(label, "") + def _get_library_target_id(label, *, scheme_name, target_ids): target_id = target_ids.get(label) if not target_id: @@ -526,17 +535,6 @@ def _pre_post_action_info_from_dicts(pre_post_actions): for pre_post_action in pre_post_actions ] -def _resolve_storekit_configuration( - label, - *, - install_path, - storekit_configurations_map): - if not label or label not in storekit_configurations_map: - return "" - path = storekit_configurations_map[label] - scheme_dir = paths.join(install_path, "xcshareddata", "xcschemes") - return relativize_unchecked(path, scheme_dir) - def _profile_info_from_dict( profile, *, @@ -596,7 +594,6 @@ def _run_info_from_dict( run, *, default_xcode_configuration, - install_path, scheme_name, storekit_configurations_map, top_level_deps): @@ -636,10 +633,9 @@ def _run_info_from_dict( env = _env_infos_from_dict(run["env"]), env_include_defaults = run["env_include_defaults"], launch_target = launch_target, - storekit_configuration = _resolve_storekit_configuration( + storekit_configuration = _storekit_configuration_info( run["storekit_configuration"], - install_path = install_path, - storekit_configurations_map = storekit_configurations_map, + storekit_configurations_map, ), xcode_configuration = xcode_configuration, ) @@ -751,7 +747,6 @@ def _scheme_info_from_dict( scheme, *, default_xcode_configuration, - install_path, storekit_configurations_map, top_level_deps): name = scheme["name"] @@ -759,7 +754,6 @@ def _scheme_info_from_dict( run = _run_info_from_dict( scheme["run"], default_xcode_configuration = default_xcode_configuration, - install_path = install_path, scheme_name = name, storekit_configurations_map = storekit_configurations_map, top_level_deps = top_level_deps, @@ -789,14 +783,12 @@ def _from_json( json_str, *, default_xcode_configuration, - install_path, storekit_configurations_map, top_level_deps): return [ _scheme_info_from_dict( scheme, default_xcode_configuration = default_xcode_configuration, - install_path = install_path, storekit_configurations_map = storekit_configurations_map, top_level_deps = top_level_deps, ) From f4fdf9127be93e6e57d3f0cddecaf0ab509abfd1 Mon Sep 17 00:00:00 2001 From: Aaron Sky Date: Wed, 19 Nov 2025 11:20:43 -0500 Subject: [PATCH 5/5] add url relativize tests Signed-off-by: Aaron Sky --- .../Generator/CreateCustomSchemeInfos.swift | 13 +++++--- .../test/CreateCustomSchemeInfosTests.swift | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 tools/generators/xcschemes/test/CreateCustomSchemeInfosTests.swift diff --git a/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift b/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift index ee070c558b..1d1e181d87 100644 --- a/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift +++ b/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift @@ -512,8 +512,9 @@ set in: url ) let storeKitConfiguration = - (try consumeArg("run-storekit-configuration", as: String?.self, in: url)).map { - // relativize the StoreKit Testing configuration file against the scheme directory within the install path + (try consumeArg("run-storekit-configuration", as: String?.self, in: url)).flatMap { + // Relativize the StoreKit Testing configuration file against + // the scheme directory within the install path. URL(filePath: $0, relativeTo: workspace) .relativize(from: schemesDirectory) } @@ -822,8 +823,12 @@ private extension SchemeInfo.LaunchTarget { } } -private extension URL { - func relativize(from source: URL) -> String { +extension URL { + func relativize(from source: URL) -> String? { + if self == source { + return self.path + } + let sourceComponents = source.deletingLastPathComponent().pathComponents let destComponents = self.pathComponents diff --git a/tools/generators/xcschemes/test/CreateCustomSchemeInfosTests.swift b/tools/generators/xcschemes/test/CreateCustomSchemeInfosTests.swift new file mode 100644 index 0000000000..0d179d919f --- /dev/null +++ b/tools/generators/xcschemes/test/CreateCustomSchemeInfosTests.swift @@ -0,0 +1,33 @@ +import XCTest + +@testable import xcschemes + +final class CreateCustomSchemeInfosTests: XCTestCase { + func test_url_relativize() { + typealias TestCase = (dest: URL, source: URL, expected: String?) + let testCases: [TestCase] = [ + // Common root + (URL(filePath: "/path/to/my/file.txt"), URL(filePath: "/path/to/your/dir"), "../my/file.txt"), + // No common root + (URL(filePath: "/path/to/my/file.txt"), URL(filePath: "/home/from/your/dir"), "../../../path/to/my/file.txt"), + // Both empty paths (implied to be /private/tmp in Bazel) + (URL(filePath: ""), URL(filePath: ""), "/private/tmp"), + // Empty destination path, absolute source path + (URL(filePath: ""), URL(filePath: "/path"), "private/tmp"), + // Absolute destination path, empty source path + (URL(filePath: "/path"), URL(filePath: ""), "../path"), + // Relative destination path (implied to be relative to /private/tmp in Bazel), absolute source path + (URL(filePath: "path/to/file.txt"), URL(filePath: "/path/to/dir"), "../../private/tmp/path/to/file.txt"), + // Absolute destination path, relative source path + (URL(filePath: "/path/to/file.txt"), URL(filePath: "path/to/dir"), "../../../../path/to/file.txt"), + // Weird relative destination path + (URL(filePath: "../../file.txt"), URL(filePath: "/path/to/dir"), "../../file.txt"), + // Absolute destination path, weird relative source path + (URL(filePath: "/path/to/file.txt"), URL(filePath: "../../to/dir"), "../path/to/file.txt"), + ] + for (dest, source, expected) in testCases { + let actual = dest.relativize(from: source) + XCTAssertEqual(expected, actual) + } + } +}