diff --git a/tools/generators/pbxnativetargets/README.md b/tools/generators/pbxnativetargets/README.md index 856635b9a9..e654195108 100644 --- a/tools/generators/pbxnativetargets/README.md +++ b/tools/generators/pbxnativetargets/README.md @@ -162,7 +162,7 @@ Here is an example output: PRODUCT_NAME = pbxtargetdependencies_tests; SDKROOT = macosx; SUPPORTED_PLATFORMS = macosx; - SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_COMPILATION_MODE = "$(BAZEL_SWIFT_COMPILATION_MODE)"; TARGET_NAME = pbxtargetdependencies_tests; }; name = Release; @@ -255,7 +255,7 @@ Here is an example output: PRODUCT_NAME = XCTestDynamicOverlay; SDKROOT = macosx; SUPPORTED_PLATFORMS = macosx; - SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_COMPILATION_MODE = "$(BAZEL_SWIFT_COMPILATION_MODE)"; TARGET_NAME = XCTestDynamicOverlay; }; name = Release; diff --git a/tools/generators/pbxnativetargets/src/Generator/CalculatePlatformVariantBuildSettings.swift b/tools/generators/pbxnativetargets/src/Generator/CalculatePlatformVariantBuildSettings.swift index 8ea0f11b1d..397df6d182 100644 --- a/tools/generators/pbxnativetargets/src/Generator/CalculatePlatformVariantBuildSettings.swift +++ b/tools/generators/pbxnativetargets/src/Generator/CalculatePlatformVariantBuildSettings.swift @@ -22,10 +22,10 @@ extension Generator { platformVariant: Target.PlatformVariant ) async throws -> [PlatformVariantBuildSetting] { return try await callable( - /*isBundle:*/ isBundle, - /*originalProductBasename:*/ originalProductBasename, - /*productType:*/ productType, - /*platformVariant:*/ platformVariant + /* isBundle: */ isBundle, + /* originalProductBasename: */ originalProductBasename, + /* productType: */ productType, + /* platformVariant: */ platformVariant ) } } @@ -146,6 +146,14 @@ extension Generator.CalculatePlatformVariantBuildSettings { } if let testHost = platformVariant.unitTestHost { + // Let Xcode select the host's native image, including its debug + // dylib. Bazel's bundle_loader points to a different executable. + buildSettings += [ + .init(key: "BUNDLE_LOADER", value: "$(BAZEL_BUNDLE_LOADER__$(BAZEL_NATIVE_PREVIEWS))".pbxProjEscaped), + .init(key: "BAZEL_BUNDLE_LOADER__", value: #""""#), + .init(key: "BAZEL_BUNDLE_LOADER__NO", value: #""""#), + .init(key: "BAZEL_BUNDLE_LOADER__YES", value: "$(TEST_HOST)".pbxProjEscaped), + ] buildSettings.append( .init( key: "TARGET_BUILD_DIR", @@ -165,6 +173,16 @@ extension Generator.CalculatePlatformVariantBuildSettings { } if let linkParams = platformVariant.linkParams { + if platformVariant.platform == .iOSSimulator { + buildSettings.append( + .init( + key: "LIBRARY_SEARCH_PATHS", + value: + #""$(inherited) $(PREVIEW_SDK_LIBRARY_SEARCH_PATH)""# + ) + ) + } + // Drop the `bazel-out` prefix since we use the env var for this // portion of the path buildSettings.append( @@ -175,15 +193,56 @@ extension Generator.CalculatePlatformVariantBuildSettings { """# ) ) - buildSettings.append( - .init( - key: "OTHER_LDFLAGS", - value: #""@$(DERIVED_FILE_DIR)/link.params""# + if productType == .staticLibrary { + // Index preparation requests compile/index inputs, not bl. + // Reuse the existing input in inactive configurations so the + // phase never requires an unprepared runtime-policy sidecar. + buildSettings += [ + .init( + key: "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX", + value: ( + "$(BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_$(ACTION)_" + + "$(BAZEL_NATIVE_PREVIEWS)_$(INDEX_ENABLE_BUILD_ARENA))" + ).pbxProjEscaped + ), + .init(key: "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_build_YES_", value: ".runtime.json"), + .init(key: "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_build_YES_NO", value: ".runtime.json"), + ] + // Xcode 26 derives a static-library target's Preview link + // closure from its Libtool task, not from OTHER_LDFLAGS. + // Create Link Dependencies truncates this response file for + // ordinary builds, preserving normal archive membership. + buildSettings.append( + .init( + key: "OTHER_LIBTOOLFLAGS", + value: #""@$(DERIVED_FILE_DIR)/link.params""# + ) ) - ) + } else { + buildSettings.append( + .init( + key: "OTHER_LDFLAGS", + value: #""-working-directory $(PROJECT_DIR) @$(DERIVED_FILE_DIR)/link.params""# + ) + ) + } } - buildSettings.append(contentsOf: platformVariant.buildSettingsFromFile) + buildSettings.append( + contentsOf: platformVariant.buildSettingsFromFile.map { buildSetting in + guard platformVariant.platform.supportsXcodePreviews, + buildSetting.key == "SWIFT_COMPILATION_MODE", + buildSetting.value == "wholemodule" + else { + return buildSetting + } + + // Unlike ENABLE_XOJIT_PREVIEWS, this configuration-scoped value + // is fixed before Preview eligibility checks. Ordinary builds + // retain WMO; only an opted-in Preview configuration is incremental. + return .init(key: buildSetting.key, value: #""$(BAZEL_SWIFT_COMPILATION_MODE)""#) + } + ) return buildSettings } @@ -216,6 +275,24 @@ private extension Platform.OS { } } +private extension Platform { + var supportsXcodePreviews: Bool { + switch self { + case .macOS, + .iOSSimulator, + .tvOSSimulator, + .visionOSSimulator, + .watchOSSimulator: + return true + case .iOSDevice, + .tvOSDevice, + .visionOSDevice, + .watchOSDevice: + return false + } + } +} + private extension PBXProductType { var fileExtension: String? { switch self { diff --git a/tools/generators/pbxnativetargets/src/Generator/CalculateXcodeConfigurationBuildSettings.swift b/tools/generators/pbxnativetargets/src/Generator/CalculateXcodeConfigurationBuildSettings.swift index 61b43433b8..0b0051ec7d 100644 --- a/tools/generators/pbxnativetargets/src/Generator/CalculateXcodeConfigurationBuildSettings.swift +++ b/tools/generators/pbxnativetargets/src/Generator/CalculateXcodeConfigurationBuildSettings.swift @@ -19,8 +19,8 @@ extension Generator { allConditionalFiles: Set ) -> [BuildSetting] { return callable( - /*platformBuildSettings:*/ platformBuildSettings, - /*allConditionalFiles:*/ allConditionalFiles + /* platformBuildSettings: */ platformBuildSettings, + /* allConditionalFiles: */ allConditionalFiles ) } } @@ -71,7 +71,7 @@ extension Generator.CalculateXcodeConfigurationBuildSettings { // Lots of code just to quote and `.pbxProjEscaped` // the paths platformBuildSettings.conditionalFiles - .map { $0.path.quoteIfNeeded } + .map(\.path.quoteIfNeeded) // TODO: See if we can not sort, or sort earlier .sorted() .joined(separator: " ") @@ -101,7 +101,7 @@ extension Generator.CalculateXcodeConfigurationBuildSettings { excludedSourceFileNames.append( contentsOf: allConditionalFiles .subtracting(conditionalFiles) - .map { $0.path.pbxProjEscaped } + .map(\.path.pbxProjEscaped) ) // Set configuration-wide conditional files diff --git a/tools/generators/pbxnativetargets/src/Generator/CreateBuildPhases.swift b/tools/generators/pbxnativetargets/src/Generator/CreateBuildPhases.swift index 867067fb07..e11a4b8652 100644 --- a/tools/generators/pbxnativetargets/src/Generator/CreateBuildPhases.swift +++ b/tools/generators/pbxnativetargets/src/Generator/CreateBuildPhases.swift @@ -204,7 +204,8 @@ extension Generator.CreateBuildPhases { buildPhases.append( createCreateLinkDependenciesBuildPhaseObject( subIdentifier: identifier.subIdentifier, - hasCompileStub: hasCompileStub + hasCompileStub: hasCompileStub, + isStaticLibrary: productType == .staticLibrary ) ) } diff --git a/tools/generators/pbxnativetargets/src/Generator/CreateCreateLinkDependenciesBuildPhaseObject.swift b/tools/generators/pbxnativetargets/src/Generator/CreateCreateLinkDependenciesBuildPhaseObject.swift index bef6e71efe..a45d7e4817 100644 --- a/tools/generators/pbxnativetargets/src/Generator/CreateCreateLinkDependenciesBuildPhaseObject.swift +++ b/tools/generators/pbxnativetargets/src/Generator/CreateCreateLinkDependenciesBuildPhaseObject.swift @@ -16,11 +16,13 @@ extension Generator { /// target. func callAsFunction( subIdentifier: Identifiers.Targets.SubIdentifier, - hasCompileStub: Bool + hasCompileStub: Bool, + isStaticLibrary: Bool = false ) -> Object { return callable( - /*subIdentifier:*/ subIdentifier, - /*hasCompileStub:*/ hasCompileStub + /* subIdentifier: */ subIdentifier, + /* hasCompileStub: */ hasCompileStub, + /* isStaticLibrary: */ isStaticLibrary ) } } @@ -31,25 +33,68 @@ extension Generator { extension Generator.CreateCreateLinkDependenciesBuildPhaseObject { typealias Callable = ( _ subIdentifier: Identifiers.Targets.SubIdentifier, - _ hasCompileStub: Bool + _ hasCompileStub: Bool, + _ isStaticLibrary: Bool ) -> Object static func defaultCallable( subIdentifier: Identifiers.Targets.SubIdentifier, - hasCompileStub: Bool + hasCompileStub: Bool, + isStaticLibrary: Bool = false ) -> Object { + let runtimeAction = isStaticLibrary ? #""" +if [[ "${BAZEL_NATIVE_PREVIEWS:-}" == "YES" && "${ACTION:-}" == build ]]; then + # Script phases have CURRENT_ARCH=undefined_arch. Use the selected target, + # never the host architecture or an unrelated ambient toolchain directory. + # Explicit exit is necessary: macOS sh can report success for a failed + # parameter expansion when an EXIT trap is installed. + for runtime_setting in ARCHS LLVM_TARGET_TRIPLE_VENDOR LLVM_TARGET_TRIPLE_OS_VERSION; do + if [[ -z "${!runtime_setting:-}" ]]; then + echo "error: Missing Preview build setting $runtime_setting" >&2 + exit 1 + fi + done + read -r -a runtime_archs <<< "$ARCHS" + if [[ "${#runtime_archs[@]}" != 1 ]]; then + echo "error: Preview runtime planning requires exactly one ARCHS value: $ARCHS" >&2 + exit 1 + fi + runtime_target="${runtime_archs[0]}-${LLVM_TARGET_TRIPLE_VENDOR}-${LLVM_TARGET_TRIPLE_OS_VERSION}" + runtime_target+="${LLVM_TARGET_TRIPLE_SUFFIX:-}" + /usr/bin/python3 "$BAZEL_INTEGRATION_DIR/preview_runtime_link_params.py" \ + --driver "${LD:-}" \ + --sdk "${SDK_DIR:-}" \ + --target "$runtime_target" \ + --swift-profile "${BAZEL_PREVIEW_SWIFT_PROFILE:-NO}" \ + --driver-policy-file "$SCRIPT_INPUT_FILE_1" >> "$link_params_tmp" +fi + +"""# : "" let action = #""" +readonly link_params_tmp="$(mktemp "$SCRIPT_OUTPUT_FILE_0.tmp.XXXXXX")" +trap 'rm -f "$link_params_tmp"' EXIT perl -pe 's/\$(\()?([a-zA-Z_]\w*)(?(1)\))/$ENV{$2}/g' \ - "$SCRIPT_INPUT_FILE_0" > "$SCRIPT_OUTPUT_FILE_0" + < "$SCRIPT_INPUT_FILE_0" > "$link_params_tmp" +\#(runtimeAction)\# +chmod 0644 "$link_params_tmp" +mv -f "$link_params_tmp" "$SCRIPT_OUTPUT_FILE_0" +trap - EXIT """# + let indexGuard = isStaticLibrary + ? #"[[ "${ACTION:-}" != indexbuild && "${INDEX_ENABLE_BUILD_ARENA:-}" != YES ]] && "# + : "" var shellScriptComponents: [String] = [ #""" set -euo pipefail -if [[ "${ENABLE_PREVIEWS:-}" == "YES" ]]; then +if \#(indexGuard)\# +[[ "${ENABLE_PREVIEWS:-}" == "YES" || \ + "${BAZEL_NATIVE_PREVIEWS:-}" == "YES" ]]; then \#(action) else - touch "$SCRIPT_OUTPUT_FILE_0" + # A prior Preview build may have populated this response file. Truncate it + # so ordinary linker and Libtool tasks cannot consume stale Preview inputs. + : > "$SCRIPT_OUTPUT_FILE_0" fi """#, @@ -58,6 +103,13 @@ fi var outputPaths = [#""" "$(DERIVED_FILE_DIR)/link.params", """#] + let runtimeInputs = isStaticLibrary ? #""" + "$(LINK_PARAMS_FILE)$(BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX)", + "$(BAZEL_INTEGRATION_DIR)/preview_runtime_link_params.py", + "$(LD)", + "$(SDK_DIR)/SDKSettings.plist", + +"""# : "" if hasCompileStub { outputPaths.append(#""" "$(DERIVED_FILE_DIR)/_CompileStub_.m", @@ -77,6 +129,7 @@ touch "$SCRIPT_OUTPUT_FILE_1" ); inputPaths = ( "$(LINK_PARAMS_FILE)", +\#(runtimeInputs)\# ); name = "Create Link Dependencies"; outputPaths = ( diff --git a/tools/generators/pbxnativetargets/test/Generator/CalculatePlatformVariantBuildSettingsTests.swift b/tools/generators/pbxnativetargets/test/Generator/CalculatePlatformVariantBuildSettingsTests.swift index f091c9307b..812a9d0250 100644 --- a/tools/generators/pbxnativetargets/test/Generator/CalculatePlatformVariantBuildSettingsTests.swift +++ b/tools/generators/pbxnativetargets/test/Generator/CalculatePlatformVariantBuildSettingsTests.swift @@ -2,11 +2,9 @@ import CustomDump import PBXProj import ToolCommon import XCTest - @testable import pbxnativetargets class CalculatePlatformVariantBuildSettingsTests: XCTestCase { - func test_base() async throws { // Arrange @@ -254,11 +252,98 @@ class CalculatePlatformVariantBuildSettingsTests: XCTestCase { linkParams: "bazel-out/some/link.params" ) + let expectedBuildSettings = baseBuildSettings.updating([ + "LINK_PARAMS_FILE": + "$(BAZEL_OUT)/some/link.params".pbxProjEscaped, + "OTHER_LIBTOOLFLAGS": + "@$(DERIVED_FILE_DIR)/link.params".pbxProjEscaped, + "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX": + "$(BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_$(ACTION)_$(BAZEL_NATIVE_PREVIEWS)_$(INDEX_ENABLE_BUILD_ARENA))".pbxProjEscaped, + "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_build_YES_": ".runtime.json", + "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_build_YES_NO": ".runtime.json", + ]) + + // Act + + let buildSettings = + try await calculatePlatformVariantBuildSettingsWithDefaults( + platformVariant: platformVariant + ) + + // Assert + + XCTAssertNoDifference( + buildSettings.asDictionary, + expectedBuildSettings + ) + } + + func test_runtimePolicyInputOnlyExistsForNativeNonIndexBuilds() async throws { + let settings = try await calculatePlatformVariantBuildSettingsWithDefaults( + platformVariant: .mock(linkParams: "bazel-out/some/link.params") + ).asDictionary + for action in ["", "build", "indexbuild", "install"] { + for native in ["", "NO", "YES"] { + for arena in ["", "NO", "YES"] { + let key = "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_\(action)_\(native)_\(arena)" + let expected = action == "build" && native == "YES" && arena != "YES" ? ".runtime.json" : "" + XCTAssertEqual(settings[key] ?? "", expected, key) + } + } + } + } + + func test_linkParams_nonStaticLibrary() async throws { + // Arrange + + let platformVariant = Target.PlatformVariant.mock( + linkParams: "bazel-out/some/link.params" + ) + let expectedBuildSettings = baseBuildSettings.updating([ "LINK_PARAMS_FILE": "$(BAZEL_OUT)/some/link.params".pbxProjEscaped, "OTHER_LDFLAGS": + "-working-directory $(PROJECT_DIR) @$(DERIVED_FILE_DIR)/link.params".pbxProjEscaped, + ]) + + // Act + + let buildSettings = + try await calculatePlatformVariantBuildSettingsWithDefaults( + originalProductBasename: "libA.dylib", + productType: .dynamicLibrary, + platformVariant: platformVariant + ) + + // Assert + + XCTAssertNoDifference( + buildSettings.asDictionary, + expectedBuildSettings + ) + } + + func test_linkParams_iOSSimulatorLibrarySearchPath() async throws { + // Arrange + + let platformVariant = Target.PlatformVariant.mock( + platform: .iOSSimulator, + linkParams: "bazel-out/some/link.params" + ) + + let expectedBuildSettings = noPlatformBuildSettings.updating([ + "IPHONEOS_DEPLOYMENT_TARGET": "9.4.1", + "LIBRARY_SEARCH_PATHS": + "$(inherited) $(PREVIEW_SDK_LIBRARY_SEARCH_PATH)".pbxProjEscaped, + "LINK_PARAMS_FILE": + "$(BAZEL_OUT)/some/link.params".pbxProjEscaped, + "OTHER_LIBTOOLFLAGS": "@$(DERIVED_FILE_DIR)/link.params".pbxProjEscaped, + "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX": + "$(BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_$(ACTION)_$(BAZEL_NATIVE_PREVIEWS)_$(INDEX_ENABLE_BUILD_ARENA))".pbxProjEscaped, + "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_build_YES_": ".runtime.json", + "BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX_build_YES_NO": ".runtime.json", ]) // Act @@ -276,6 +361,35 @@ class CalculatePlatformVariantBuildSettingsTests: XCTestCase { ) } + func test_librarySearchPathExcludedFromOtherLinkModes() async throws { + for platformVariant in [ + Target.PlatformVariant.mock( + platform: .iOSDevice, + linkParams: "bazel-out/some/link.params" + ), + Target.PlatformVariant.mock( + platform: .tvOSSimulator, + linkParams: "bazel-out/some/link.params" + ), + Target.PlatformVariant.mock( + platform: .visionOSSimulator, + linkParams: "bazel-out/some/link.params" + ), + Target.PlatformVariant.mock( + platform: .watchOSSimulator, + linkParams: "bazel-out/some/link.params" + ), + Target.PlatformVariant.mock(platform: .iOSSimulator), + ] { + let buildSettings = + try await calculatePlatformVariantBuildSettingsWithDefaults( + platformVariant: platformVariant + ) + + XCTAssertNil(buildSettings.asDictionary["LIBRARY_SEARCH_PATHS"]) + } + } + func test_packageBinDir() async throws { // Arrange @@ -378,6 +492,10 @@ $(BUILD_DIR)/some/packageBin/dir$(TARGET_BUILD_SUBPATH) "TEST_HOST": #""" $(BUILD_DIR)/some/packageBin/dir/a/path/Host.app/Executable_Name """#.pbxProjEscaped, + "BUNDLE_LOADER": "$(BAZEL_BUNDLE_LOADER__$(BAZEL_NATIVE_PREVIEWS))".pbxProjEscaped, + "BAZEL_BUNDLE_LOADER__": #""""#, + "BAZEL_BUNDLE_LOADER__NO": #""""#, + "BAZEL_BUNDLE_LOADER__YES": "$(TEST_HOST)".pbxProjEscaped, ]) // Act @@ -429,7 +547,7 @@ $(BUILD_DIR)/some/packageBin/dir/a/path/Host.app/Executable_Name } } -private func calculatePlatformVariantBuildSettingsWithDefaults( +func calculatePlatformVariantBuildSettingsWithDefaults( isBundle: Bool = false, originalProductBasename: String = "libA.a", productType: PBXProductType = .staticLibrary, @@ -457,7 +575,7 @@ private let baseBuildSettings = noPlatformBuildSettings.updating([ "MACOSX_DEPLOYMENT_TARGET": "9.4.1", ]) -private extension Target.PlatformVariant { +extension Target.PlatformVariant { static func mock( xcodeConfigurations: [String] = ["CONFIG"], id: TargetID = "A", @@ -501,7 +619,7 @@ private extension Target.PlatformVariant { } } -private extension Array where Element == PlatformVariantBuildSetting { +extension Array where Element == PlatformVariantBuildSetting { var asDictionary: [String: String] { return Dictionary(uniqueKeysWithValues: map { ($0.key, $0.value) }) } diff --git a/tools/generators/pbxnativetargets/test/Generator/CalculateXcodeConfigurationBuildSettingsTests.swift b/tools/generators/pbxnativetargets/test/Generator/CalculateXcodeConfigurationBuildSettingsTests.swift index 4395893393..bbd62fd836 100644 --- a/tools/generators/pbxnativetargets/test/Generator/CalculateXcodeConfigurationBuildSettingsTests.swift +++ b/tools/generators/pbxnativetargets/test/Generator/CalculateXcodeConfigurationBuildSettingsTests.swift @@ -1,7 +1,6 @@ import CustomDump import PBXProj import XCTest - @testable import pbxnativetargets final class CalculateXcodeConfigurationBuildSettingsTests: XCTestCase { @@ -80,5 +79,74 @@ final class CalculateXcodeConfigurationBuildSettingsTests: XCTestCase { ) } + func test_compilationModePreservesSDKValuesInEitherInputOrder() { + for (simulator, device) in [ + (Platform.iOSSimulator, Platform.iOSDevice), + (.tvOSSimulator, .tvOSDevice), + (.visionOSSimulator, .visionOSDevice), + (.watchOSSimulator, .watchOSDevice), + ] { + let simulatorSettings = compilationSettings(simulator, "singlefile") + let deviceSettings = compilationSettings(device, "wholemodule") + for settings in [ + [simulatorSettings, deviceSettings], + [deviceSettings, simulatorSettings], + ] { + let actual = Generator.CalculateXcodeConfigurationBuildSettings + .defaultCallable( + platformBuildSettings: settings, + allConditionalFiles: [] + ) + let values = actual.asDictionary + for (platform, expected) in [ + (simulator, "singlefile"), + (device, "wholemodule"), + ] { + let conditionalKey = + "SWIFT_COMPILATION_MODE[sdk=\(platform.rawValue)*]".quoted + XCTAssertEqual( + values[conditionalKey] ?? values["SWIFT_COMPILATION_MODE"], + expected, + "platform: \(platform), order: \(settings.map(\.platform))" + ) + } + } + } + } + + func test_compilationModePreservesSinglePlatformAndInheritedDefaults() { + for (platform, mode) in [ + (Platform.iOSDevice, "wholemodule"), + (.macOS, "singlefile"), + ] { + let actual = Generator.CalculateXcodeConfigurationBuildSettings + .defaultCallable( + platformBuildSettings: [compilationSettings(platform, mode)], + allConditionalFiles: [] + ) + XCTAssertNoDifference(actual.asDictionary, ["SWIFT_COMPILATION_MODE": mode]) + } + + let actual = Generator.CalculateXcodeConfigurationBuildSettings + .defaultCallable( + platformBuildSettings: [ + compilationSettings(.iOSSimulator, "singlefile"), + .init(platform: .iOSDevice, conditionalFiles: [], buildSettings: []), + ], + allConditionalFiles: [] + ) + XCTAssertNoDifference(actual.asDictionary, [ + "SWIFT_COMPILATION_MODE[sdk=iphonesimulator*]".quoted: "singlefile", + ]) + } + + private func compilationSettings(_ platform: Platform, _ mode: String) -> PlatformBuildSettings { + return .init( + platform: platform, + conditionalFiles: [], + buildSettings: [.init(key: "SWIFT_COMPILATION_MODE", value: mode)] + ) + } + // FIXME: test_conditionalFiles } diff --git a/tools/generators/pbxnativetargets/test/Generator/CreateCreateLinkDependenciesBuildPhaseObjectTests.swift b/tools/generators/pbxnativetargets/test/Generator/CreateCreateLinkDependenciesBuildPhaseObjectTests.swift index bc8507c1fd..ac5c8c047d 100644 --- a/tools/generators/pbxnativetargets/test/Generator/CreateCreateLinkDependenciesBuildPhaseObjectTests.swift +++ b/tools/generators/pbxnativetargets/test/Generator/CreateCreateLinkDependenciesBuildPhaseObjectTests.swift @@ -1,11 +1,18 @@ import CustomDump +import Foundation import ToolCommon import XCTest - @testable import pbxnativetargets @testable import PBXProj +// Serialized PBX shell scripts are intentionally represented as one line. +// swiftlint:disable line_length class CreateCreateLinkDependenciesBuildPhaseObjectTests: XCTestCase { + private let previewModes = [ + ["ENABLE_PREVIEWS": "YES", "BAZEL_NATIVE_PREVIEWS": "NO"], + ["ENABLE_PREVIEWS": "NO", "BAZEL_NATIVE_PREVIEWS": "YES"], + ] + func test_base() { // Arrange @@ -35,7 +42,7 @@ A_SHARD00A_HASH000000000005 /* Create Link Dependencies */ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -euo pipefail\n\nif [[ \"${ENABLE_PREVIEWS:-}\" == \"YES\" ]]; then\nperl -pe 's/\\$(\\()?([a-zA-Z_]\\w*)(?(1)\\))/$ENV{$2}/g' \\\n \"$SCRIPT_INPUT_FILE_0\" > \"$SCRIPT_OUTPUT_FILE_0\"\nelse\n touch \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n"; + shellScript = "set -euo pipefail\n\nif [[ \"${ENABLE_PREVIEWS:-}\" == \"YES\" || \\\n \"${BAZEL_NATIVE_PREVIEWS:-}\" == \"YES\" ]]; then\nreadonly link_params_tmp=\"$(mktemp \"$SCRIPT_OUTPUT_FILE_0.tmp.XXXXXX\")\"\ntrap 'rm -f \"$link_params_tmp\"' EXIT\nperl -pe 's/\\$(\\()?([a-zA-Z_]\\w*)(?(1)\\))/$ENV{$2}/g' \\\n < \"$SCRIPT_INPUT_FILE_0\" > \"$link_params_tmp\"\nchmod 0644 \"$link_params_tmp\"\nmv -f \"$link_params_tmp\" \"$SCRIPT_OUTPUT_FILE_0\"\ntrap - EXIT\nelse\n # A prior Preview build may have populated this response file. Truncate it\n # so ordinary linker and Libtool tasks cannot consume stale Preview inputs.\n : > \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n"; showEnvVarsInLog = 0; } """# @@ -84,7 +91,7 @@ A_SHARD00A_HASH000000000005 /* Create Link Dependencies */ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -euo pipefail\n\nif [[ \"${ENABLE_PREVIEWS:-}\" == \"YES\" ]]; then\nperl -pe 's/\\$(\\()?([a-zA-Z_]\\w*)(?(1)\\))/$ENV{$2}/g' \\\n \"$SCRIPT_INPUT_FILE_0\" > \"$SCRIPT_OUTPUT_FILE_0\"\nelse\n touch \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n\ntouch \"$SCRIPT_OUTPUT_FILE_1\"\n"; + shellScript = "set -euo pipefail\n\nif [[ \"${ENABLE_PREVIEWS:-}\" == \"YES\" || \\\n \"${BAZEL_NATIVE_PREVIEWS:-}\" == \"YES\" ]]; then\nreadonly link_params_tmp=\"$(mktemp \"$SCRIPT_OUTPUT_FILE_0.tmp.XXXXXX\")\"\ntrap 'rm -f \"$link_params_tmp\"' EXIT\nperl -pe 's/\\$(\\()?([a-zA-Z_]\\w*)(?(1)\\))/$ENV{$2}/g' \\\n < \"$SCRIPT_INPUT_FILE_0\" > \"$link_params_tmp\"\nchmod 0644 \"$link_params_tmp\"\nmv -f \"$link_params_tmp\" \"$SCRIPT_OUTPUT_FILE_0\"\ntrap - EXIT\nelse\n # A prior Preview build may have populated this response file. Truncate it\n # so ordinary linker and Libtool tasks cannot consume stale Preview inputs.\n : > \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n\ntouch \"$SCRIPT_OUTPUT_FILE_1\"\n"; showEnvVarsInLog = 0; } """# @@ -102,4 +109,272 @@ A_SHARD00A_HASH000000000005 /* Create Link Dependencies */ XCTAssertNoDifference(object, expectedObject) } + + func test_missingPreviewInputPreservesResponse() throws { + for previewSettings in previewModes { + for hasCompileStub in [false, true] { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory( + at: directory, + withIntermediateDirectories: true + ) + defer { try? FileManager.default.removeItem(at: directory) } + + let output = directory.appendingPathComponent("link.params") + try "previous response\n".write( + to: output, + atomically: true, + encoding: .utf8 + ) + + let status = try runScript( + directory: directory, + previewSettings: previewSettings, + hasCompileStub: hasCompileStub + ) + + XCTAssertNotEqual(status, 0) + XCTAssertEqual( + try String(contentsOf: output), + "previous response\n" + ) + XCTAssertEqual( + try FileManager.default.contentsOfDirectory( + atPath: directory.path + ).sorted(), + ["link.params"] + ) + } + } + } + + func test_previewInputExpandsResponse() throws { + for previewSettings in previewModes { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory( + at: directory, + withIntermediateDirectories: true + ) + defer { try? FileManager.default.removeItem(at: directory) } + + try "-L$(PROJECT_DIR)/libraries\n".write( + to: directory.appendingPathComponent("input.params"), + atomically: true, + encoding: .utf8 + ) + XCTAssertEqual( + try runScript( + directory: directory, + previewSettings: previewSettings, + hasCompileStub: true + ), + 0 + ) + XCTAssertEqual( + try String( + contentsOf: directory.appendingPathComponent("link.params") + ), + "-L/source root/libraries\n" + ) + XCTAssertEqual( + try FileManager.default.contentsOfDirectory( + atPath: directory.path + ).sorted(), + ["_CompileStub_.m", "input.params", "link.params"] + ) + } + } + + func test_ordinaryBuildClearsStalePreviewResponseWithoutInput() throws { + let ordinaryModes: [[String: String]] = [ + [:], + ["ENABLE_XOJIT_PREVIEWS": "YES"], + ["BAZEL_NATIVE_PREVIEWS": "NO", "ENABLE_XOJIT_PREVIEWS": "YES"], + ["ENABLE_PREVIEWS": "NO"], + ["BAZEL_NATIVE_PREVIEWS": "NO"], + ["ENABLE_PREVIEWS": "NO", "BAZEL_NATIVE_PREVIEWS": "NO"], + ] + for previewSettings in ordinaryModes { + for hasCompileStub in [false, true] { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory( + at: directory, + withIntermediateDirectories: true + ) + defer { try? FileManager.default.removeItem(at: directory) } + + let output = directory.appendingPathComponent("link.params") + try "stale Preview dependency\n".write( + to: output, + atomically: true, + encoding: .utf8 + ) + XCTAssertEqual( + try runScript( + directory: directory, + previewSettings: previewSettings, + hasCompileStub: hasCompileStub + ), + 0 + ) + XCTAssertEqual(try String(contentsOf: output), "") + XCTAssertEqual( + try FileManager.default.contentsOfDirectory( + atPath: directory.path + ).sorted(), + hasCompileStub + ? ["_CompileStub_.m", "link.params"] + : ["link.params"] + ) + } + } + } + + func test_nativeStaticRuntimeStagingUsesSelectedEnvironmentAndLiteralPolicy() throws { + let directory = try runtimeDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + let output = directory.appendingPathComponent("link.params") + let flags = "[\"-fprofile-generate=Author's profile dir\",\"-nodefaultlibs\"]" + try flags.write(to: directory.appendingPathComponent("policy.json"), atomically: true, encoding: .utf8) + XCTAssertEqual(try runScript(directory: directory, previewSettings: runtimeEnvironment(directory), hasCompileStub: true, isStaticLibrary: true), 0) + XCTAssertEqual(try String(contentsOf: output), "-L/source root/libraries\n\"/selected runtime.a\"\n") + let received = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(contentsOf: directory.appendingPathComponent("received.json"))) as? [String: Any]) + XCTAssertEqual(received["args"] as? [String], [ + "--driver", "/selected toolchain/usr/bin/clang", "--sdk", "/selected SDK.sdk", + "--target", "arm64-apple-ios16.0-simulator", "--swift-profile", "YES", + "--driver-policy-file", directory.appendingPathComponent("policy.json").path, + ]) + XCTAssertEqual(received["policy"] as? [String], ["-fprofile-generate=Author's profile dir", "-nodefaultlibs"]) + XCTAssertTrue(FileManager.default.fileExists(atPath: directory.appendingPathComponent("_CompileStub_.m").path)) + } + + func test_runtimePlanningFailurePreservesPriorResponse() throws { + for overrides in [["FAIL_RUNTIME_QUERY": "YES"], ["ARCHS": "arm64 x86_64"], ["ARCHS": ""], ["LD": ""], ["SDK_DIR": ""], ["LLVM_TARGET_TRIPLE_OS_VERSION": ""]] { + let directory = try runtimeDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + let output = directory.appendingPathComponent("link.params") + try "previous response\n".write(to: output, atomically: true, encoding: .utf8) + let environment = runtimeEnvironment(directory).merging(overrides) { _, value in value } + XCTAssertNotEqual(try runScript(directory: directory, previewSettings: environment, hasCompileStub: false, isStaticLibrary: true), 0, "Failed validation for \(overrides)") + if overrides["FAIL_RUNTIME_QUERY"] == "YES" { + // A failure to start Python must not satisfy this control. + XCTAssertTrue(FileManager.default.fileExists(atPath: directory.appendingPathComponent("received.json").path)) + } + XCTAssertEqual(try String(contentsOf: output), "previous response\n") + XCTAssertFalse(try FileManager.default.contentsOfDirectory(atPath: directory.path).contains { $0.hasPrefix("link.params.tmp.") }) + } + } + + func test_runtimeQueryIsAbsentForOrdinaryLegacyNonStaticAndIndexing() throws { + for (isStatic, overrides, expected) in [ + (true, ["BAZEL_NATIVE_PREVIEWS": "NO", "ENABLE_PREVIEWS": "NO"], ""), + (true, ["BAZEL_NATIVE_PREVIEWS": "NO", "ENABLE_PREVIEWS": "YES"], "-L/source root/libraries\n"), + (false, [String: String](), "-L/source root/libraries\n"), + (true, ["ACTION": "indexbuild"], ""), + (true, ["ACTION": "indexbuild", "INDEX_ENABLE_BUILD_ARENA": "NO"], ""), + (true, ["INDEX_ENABLE_BUILD_ARENA": "YES"], ""), + (true, ["ACTION": "install"], "-L/source root/libraries\n"), + ] { + let directory = try runtimeDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + // No prepared policy is needed in any inactive mode. + try FileManager.default.removeItem(at: directory.appendingPathComponent("policy.json")) + let environment = runtimeEnvironment(directory).merging(overrides) { _, value in value } + XCTAssertEqual(try runScript(directory: directory, previewSettings: environment, hasCompileStub: false, isStaticLibrary: isStatic), 0) + XCTAssertEqual(try String(contentsOf: directory.appendingPathComponent("link.params")), expected) + XCTAssertFalse(FileManager.default.fileExists(atPath: directory.appendingPathComponent("received.json").path)) + } + } + + func test_staticPolicyInputIsConditionalAndHelperIsDeclared() throws { + let object = Generator.CreateCreateLinkDependenciesBuildPhaseObject.defaultCallable( + subIdentifier: .init(shard: "A_SHARD", hash: "A_HASH"), hasCompileStub: true, isStaticLibrary: true + ) + let plist = try XCTUnwrap(PropertyListSerialization.propertyList(from: Data(object.content.utf8), format: nil) as? [String: Any]) + XCTAssertEqual(plist["inputPaths"] as? [String], [ + "$(LINK_PARAMS_FILE)", "$(LINK_PARAMS_FILE)$(BAZEL_PREVIEW_RUNTIME_POLICY_SUFFIX)", + "$(BAZEL_INTEGRATION_DIR)/preview_runtime_link_params.py", + "$(LD)", "$(SDK_DIR)/SDKSettings.plist", + ]) + XCTAssertEqual(plist["outputPaths"] as? [String], ["$(DERIVED_FILE_DIR)/link.params", "$(DERIVED_FILE_DIR)/_CompileStub_.m"]) + } + + private func runtimeDirectory() throws -> URL { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent("runtime's \(UUID().uuidString)") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + try "-L$(PROJECT_DIR)/libraries\n".write(to: directory.appendingPathComponent("input.params"), atomically: true, encoding: .utf8) + try "[]".write(to: directory.appendingPathComponent("policy.json"), atomically: true, encoding: .utf8) + // Mock only the planning boundary; kernel tests execute its parser and + // real selected-driver controls separately. No Xcode/runtime is started. + try #""" +import json, os, sys +from pathlib import Path +root = Path(__file__).parent +args = sys.argv[1:] +if not args[args.index("--sdk") + 1] or not args[args.index("--driver") + 1]: + sys.exit(1) +policy = json.loads(Path(args[args.index("--driver-policy-file") + 1]).read_text()) +(root / "received.json").write_text(json.dumps({"args": args, "policy": policy})) +if os.environ.get("FAIL_RUNTIME_QUERY") == "YES": + sys.exit(1) +print('"/selected runtime.a"') +"""#.write(to: directory.appendingPathComponent("preview_runtime_link_params.py"), atomically: true, encoding: .utf8) + return directory + } + + private func runtimeEnvironment(_ directory: URL) -> [String: String] { + [ + "BAZEL_NATIVE_PREVIEWS": "YES", "ENABLE_PREVIEWS": "NO", "ACTION": "build", + "BAZEL_INTEGRATION_DIR": directory.path, "SCRIPT_INPUT_FILE_1": directory.appendingPathComponent("policy.json").path, + "ARCHS": "arm64", "LD": "/selected toolchain/usr/bin/clang", "SDK_DIR": "/selected SDK.sdk", + "LLVM_TARGET_TRIPLE_VENDOR": "apple", "LLVM_TARGET_TRIPLE_OS_VERSION": "ios16.0", "LLVM_TARGET_TRIPLE_SUFFIX": "-simulator", + "BAZEL_PREVIEW_SWIFT_PROFILE": "YES", "CURRENT_ARCH": "undefined_arch", "NATIVE_ARCH_ACTUAL": "arm64e", + // /usr/bin/python3 resolves SDKROOT before starting the helper. + // Use an installed SDK, deliberately different from SDK_DIR, so + // this fixture also runs on Xcodes without the iOS 26.5 SDK. + "TOOLCHAIN_DIR": "/misleading metal toolchain", "SDKROOT": "macosx", + ] + } + + private func runScript( + directory: URL, + previewSettings: [String: String], + hasCompileStub: Bool, + isStaticLibrary: Bool = false + ) throws -> Int32 { + let object = Generator.CreateCreateLinkDependenciesBuildPhaseObject + .defaultCallable( + subIdentifier: .init(shard: "A_SHARD", hash: "A_HASH"), + hasCompileStub: hasCompileStub, + isStaticLibrary: isStaticLibrary + ) + let plist = try XCTUnwrap( + PropertyListSerialization.propertyList( + from: Data(object.content.utf8), + format: nil + ) as? [String: Any] + ) + let script = try XCTUnwrap(plist["shellScript"] as? String) + let process = Process() + process.executableURL = URL(fileURLWithPath: "/bin/sh") + process.arguments = ["-c", script] + process.environment = [ + "PATH": "/usr/bin:/bin", + "PROJECT_DIR": "/source root", + "SCRIPT_INPUT_FILE_0": directory + .appendingPathComponent("input.params").path, + "SCRIPT_OUTPUT_FILE_0": directory + .appendingPathComponent("link.params").path, + "SCRIPT_OUTPUT_FILE_1": directory + .appendingPathComponent("_CompileStub_.m").path, + ].merging(previewSettings) { _, value in value } + try process.run() + process.waitUntilExit() + return process.terminationStatus + } } + +// swiftlint:enable line_length diff --git a/tools/generators/pbxnativetargets/test/Generator/PreviewCompilationModeBuildSettingsTests.swift b/tools/generators/pbxnativetargets/test/Generator/PreviewCompilationModeBuildSettingsTests.swift new file mode 100644 index 0000000000..2c2c37ee2e --- /dev/null +++ b/tools/generators/pbxnativetargets/test/Generator/PreviewCompilationModeBuildSettingsTests.swift @@ -0,0 +1,91 @@ +import ToolCommon +import XCTest +@testable import pbxnativetargets +@testable import PBXProj + +class PreviewCompilationModeBuildSettingsTests: XCTestCase { + func test_previewPlatformsUseConfigurationScopedCompilationForWMO() async throws { + for platform in [ + Platform.macOS, + .iOSSimulator, + .tvOSSimulator, + .visionOSSimulator, + .watchOSSimulator, + ] { + let buildSettings = + try await calculatePlatformVariantBuildSettingsWithDefaults( + platformVariant: .mock( + platform: platform, + buildSettingsFromFile: [ + .init( + key: "SWIFT_COMPILATION_MODE", + value: "wholemodule" + ), + .init(key: "OTHER_SETTING", value: "VALUE"), + ] + ) + ) + + XCTAssertEqual( + buildSettings.asDictionary["SWIFT_COMPILATION_MODE"], + #""$(BAZEL_SWIFT_COMPILATION_MODE)""#, + "platform: \(platform)" + ) + XCTAssertEqual( + buildSettings.asDictionary["OTHER_SETTING"], + "VALUE", + "platform: \(platform)" + ) + } + } + + func test_devicePlatformsPreserveWholeModuleCompilation() async throws { + for platform in [ + Platform.iOSDevice, + .tvOSDevice, + .visionOSDevice, + .watchOSDevice, + ] { + let buildSettings = + try await calculatePlatformVariantBuildSettingsWithDefaults( + platformVariant: .mock( + platform: platform, + buildSettingsFromFile: [ + .init( + key: "SWIFT_COMPILATION_MODE", + value: "wholemodule" + ), + ] + ) + ) + + XCTAssertEqual( + buildSettings.asDictionary["SWIFT_COMPILATION_MODE"], + "wholemodule", + "platform: \(platform)" + ) + } + } + + func test_previewPlatformsPreserveOtherCompilationModes() async throws { + for compilationMode in ["singlefile", "unknown"] { + let buildSettings = + try await calculatePlatformVariantBuildSettingsWithDefaults( + platformVariant: .mock( + platform: .iOSSimulator, + buildSettingsFromFile: [ + .init( + key: "SWIFT_COMPILATION_MODE", + value: compilationMode + ), + ] + ) + ) + + XCTAssertEqual( + buildSettings.asDictionary["SWIFT_COMPILATION_MODE"], + compilationMode + ) + } + } +} diff --git a/tools/generators/pbxnativetargets/test/Generator/StaticLibraryPreviewLinkContractTests.swift b/tools/generators/pbxnativetargets/test/Generator/StaticLibraryPreviewLinkContractTests.swift new file mode 100644 index 0000000000..0925ec5bdd --- /dev/null +++ b/tools/generators/pbxnativetargets/test/Generator/StaticLibraryPreviewLinkContractTests.swift @@ -0,0 +1,86 @@ +import ToolCommon +import XCTest +@testable import pbxnativetargets +@testable import PBXProj + +class StaticLibraryPreviewLinkContractTests: XCTestCase { + func test_staticLibraryUsesCreateLinkDependenciesAndLibtoolFlags() async throws { + let identifier = Identifiers.Targets.Identifier( + pbxProjEscapedName: "Subject", + subIdentifier: .init(shard: "A_SHARD", hash: "A_HASH"), + full: "A_ID /* Subject */", + withoutComment: "A_ID" + ) + let createBuildPhases = Generator.CreateBuildPhases( + createBazelIntegrationBuildPhaseObject: + Generator.CreateBazelIntegrationBuildPhaseObject(), + createBuildFileSubIdentifier: + Generator.CreateBuildFileSubIdentifier(), + createCreateCompileDependenciesBuildPhaseObject: + Generator.CreateCreateCompileDependenciesBuildPhaseObject(), + createCreateLinkDependenciesBuildPhaseObject: + Generator.CreateCreateLinkDependenciesBuildPhaseObject(), + createEmbedAppExtensionsBuildPhaseObject: + Generator.CreateEmbedAppExtensionsBuildPhaseObject(), + createProductBuildFileObject: + Generator.CreateProductBuildFileObject(), + createSourcesBuildPhaseObject: + Generator.CreateSourcesBuildPhaseObject() + ) + + let phases = createBuildPhases( + consolidatedInputs: .init(srcs: [], nonArcSrcs: []), + hasCParams: false, + hasCxxParams: false, + hasLinkParams: true, + identifier: identifier, + productType: .staticLibrary, + shard: 0, + usesInfoPlist: false, + watchKitExtensionProductIdentifier: nil + ).buildPhases + + XCTAssertTrue( + phases.contains { + $0.content.contains("name = \"Create Link Dependencies\";") + } + ) + + let platformVariant = Target.PlatformVariant( + xcodeConfigurations: ["Debug"], + id: "ID config", + bundleID: nil, + compileTargetIDs: nil, + packageBinDir: "bazel-out/package", + outputsProductPath: nil, + productName: "Subject", + productBasename: "libSubject.a", + moduleName: "Subject", + platform: .iOSSimulator, + osVersion: "17.0", + arch: "arm64", + executableName: nil, + conditionalFiles: [], + buildSettingsFromFile: [], + linkParams: "bazel-out/package/Subject.preview.link.params", + unitTestHost: nil, + dSYMPathsBuildSetting: nil + ) + let settings = try await Generator + .CalculatePlatformVariantBuildSettings.defaultCallable( + isBundle: false, + originalProductBasename: "libSubject.a", + productType: .staticLibrary, + platformVariant: platformVariant + ) + let settingsByKey = Dictionary( + uniqueKeysWithValues: settings.map { ($0.key, $0.value) } + ) + + XCTAssertEqual( + settingsByKey["OTHER_LIBTOOLFLAGS"], + #""@$(DERIVED_FILE_DIR)/link.params""# + ) + XCTAssertNil(settingsByKey["OTHER_LDFLAGS"]) + } +}