From 97d36fd1d29b69f0786c5d38acf1535b8776d233 Mon Sep 17 00:00:00 2001 From: Giovanni Amati Date: Tue, 31 Oct 2023 09:55:38 +0000 Subject: [PATCH] Support for multiple deployment targets with xcode 14 (#1336) * platformFilters on Dependecies * platformFilters on sources * fixed current unit tests * renamed enum to SupportedPlatforms * supportedPlatforms field for target * errors * renamed errors * inferPlatformFiltersByPath flag * changed priority to generate filter * fixed parsing * fixed init * unit test supportedPlatforms * unit tests for errors * fixing build settings and unit tests * added new settingsPresets * new check errors and unit tests * case insensitive match * fixed skipping cross platform target * json decode * unit tests inferPlatformFiltersByPath and platformFilters for sources * mocked files * fixing unit tests * first test on dependecies * unit tests completed * fixed unit tests * changelog * doc changes * doc changes * doc changes * doc changes * doc changes * doc changes * doc changes * doc changes * fixed doc * fixed unti tests style * fixed regex * fixed doc * addressing comments * Added TestProject, moved unit tests resources in another folder * Raising error if platform is an array * unit test on new error * fixed error enum * Integrated in TestProject * committed TestProject * unit test error * fixing spm deps in test project * pushed testProject * pushed testProject * pushed testProject fix * comment on isResolved property * renameing supportedPlatforms to supportedDestinations * renameing supportedPlatforms to supportedDestinations * renameing test app * checked out old file * fixing test app * working on auto baseSDK * fixed deploymentTarget * renamed errors * fixed presets * remamed index to priority * small comments * removed isResolved in target and fixed error check * added unit tests * fixed doc * fixed doc * fixed doc * fixed doc * fixed test app * add visionOS and more error check and testing * fixed supported destinations priority and tests * fixed doc * solved conflicts * fixed conflicts * renamed everything --------- Co-authored-by: Giovanni Amati --- CHANGELOG.md | 9 + Docs/ProjectSpec.md | 54 ++- SettingPresets/Platforms/tvOS.yml | 2 +- SettingPresets/SupportedDestinations/iOS.yml | 5 + .../SupportedDestinations/macCatalyst.yml | 2 + .../SupportedDestinations/macOS.yml | 3 + SettingPresets/SupportedDestinations/tvOS.yml | 2 + .../SupportedDestinations/visionOS.yml | 3 + Sources/ProjectSpec/Dependency.swift | 12 +- Sources/ProjectSpec/DeploymentTarget.swift | 3 + Sources/ProjectSpec/Platform.swift | 3 +- Sources/ProjectSpec/Plist.swift | 2 +- Sources/ProjectSpec/Settings.swift | 2 +- Sources/ProjectSpec/SpecParsingError.swift | 3 + Sources/ProjectSpec/SpecValidation.swift | 23 + Sources/ProjectSpec/SpecValidationError.swift | 12 + .../ProjectSpec/SupportedDestination.swift | 45 ++ Sources/ProjectSpec/Target.swift | 44 +- Sources/ProjectSpec/TargetSource.swift | 16 +- Sources/ProjectSpec/TestPlan.swift | 2 +- Sources/ProjectSpec/XCProjExtensions.swift | 1 + .../CarthageDependencyResolver.swift | 3 + Sources/XcodeGenKit/PBXProjGenerator.swift | 37 +- Sources/XcodeGenKit/SettingsBuilder.swift | 59 ++- Sources/XcodeGenKit/SettingsPresetFile.swift | 3 + Sources/XcodeGenKit/SourceGenerator.swift | 24 +- .../Info.generated.plist | 24 + .../Sources/MyAppApp.swift | 10 + .../Sources/iOS/ContentView.swift | 13 + .../Sources/tvOS/ContentView.swift | 13 + .../Storyboards/LaunchScreen.storyboard | 25 + .../TestResources/File_MACCATALYST.swift | 1 + .../TestResources/File_ios.swift | 1 + .../TestResources/File_macOS.swift | 1 + .../TestResources/File_tvOs.swift | 1 + .../TestResources/TVOS/File_B.swift | 1 + .../TestResources/iOs/File_A.swift | 1 + .../TestResources/macCatalyst/File_D.swift | 1 + .../TestResources/macos/File_C.swift | 1 + .../Project.xcodeproj/project.pbxproj | 242 ++++++++++ Tests/Fixtures/TestProject/project.yml | 25 +- Tests/ProjectSpecTests/ProjectSpecTests.swift | 65 ++- Tests/ProjectSpecTests/SpecLoadingTests.swift | 58 ++- .../ProjectGeneratorTests.swift | 444 +++++++++++++++++- 44 files changed, 1255 insertions(+), 46 deletions(-) create mode 100644 SettingPresets/SupportedDestinations/iOS.yml create mode 100644 SettingPresets/SupportedDestinations/macCatalyst.yml create mode 100644 SettingPresets/SupportedDestinations/macOS.yml create mode 100644 SettingPresets/SupportedDestinations/tvOS.yml create mode 100644 SettingPresets/SupportedDestinations/visionOS.yml create mode 100644 Sources/ProjectSpec/SupportedDestination.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/Info.generated.plist create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/Sources/MyAppApp.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/Sources/iOS/ContentView.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/Sources/tvOS/ContentView.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/Storyboards/LaunchScreen.storyboard create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_MACCATALYST.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_ios.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_macOS.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_tvOs.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/TVOS/File_B.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/iOs/File_A.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/macCatalyst/File_D.swift create mode 100644 Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/macos/File_C.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae404e2..28e51b16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Next Version +### Feature support for multiple deployment targets with xcode 14 + +- Added `supportedDestinations` for target +- Added a new platform value `auto` that we can use only with `supportedDestinations` +- Added the possiblity to avoid the definition of plaform, only when we use `supportedDestinations`, that fallbacks to 'auto' +- Added `destinationFilters` for sources and dependecies +- Added `inferDestinationFiltersByPath`, a convenience filter for sources +- Added visionOS support + ### Added - `.mlpackage` files now default to being a source type #1398 @aaron-foreflight diff --git a/Docs/ProjectSpec.md b/Docs/ProjectSpec.md index c9d400f4..326e17f3 100644 --- a/Docs/ProjectSpec.md +++ b/Docs/ProjectSpec.md @@ -22,6 +22,7 @@ You can also use environment variables in your configuration file, by using `${S - [Target](#target) - [Product Type](#product-type) - [Platform](#platform) + - [Supported Destinations](#supported-destinations) - [Sources](#sources) - [Target Source](#target-source) - [Dependency](#dependency) @@ -356,6 +357,7 @@ Settings are merged in the following order: `groups`, `base`, `configs` (simple - [x] **type**: **[Product Type](#product-type)** - Product type of the target - [x] **platform**: **[Platform](#platform)** - Platform of the target +- [ ] **supportedDestinations**: **[[Supported Destinations](#supported-destinations)]** - List of supported platform destinations for the target. - [ ] **deploymentTarget**: **String** - The deployment target (eg `9.2`). If this is not specified the value from the project set in [Options](#options)`.deploymentTarget.PLATFORM` will be used. - [ ] **sources**: **[Sources](#sources)** - Source directories of the target - [ ] **configFiles**: **[Config Files](#config-files)** - `.xcconfig` files per config @@ -433,12 +435,15 @@ This will provide default build settings for a certain product type. It can be a This will provide default build settings for a certain platform. It can be any of the following: +- `auto` (available only when we use `supportedDestinations`) - `iOS` -- `macOS` - `tvOS` +- `macOS` - `watchOS` - `visionOS` (`visionOS` doesn't support Carthage usage) +Note that when we use supported destinations with Xcode 14+ we can avoid the definition of platform that fallbacks to the `auto` value. + **Multi Platform targets** You can also specify an array of platforms. This will generate a target for each platform. @@ -469,6 +474,33 @@ targets: The above will generate 2 targets named `MyFramework_iOS` and `MyFramework_tvOS`, with all the relevant platform build settings. They will both have a `PRODUCT_NAME` of `MyFramework` +### Supported Destinations + +This will provide a mix of default build settings for the chosen platform destinations. It can be any of the following: + +- `iOS` +- `tvOS` +- `macOS` +- `macCatalyst` +- `visionOS` + +```yaml +targets: + MyFramework: + type: framework + supportedDestinations: [iOS, tvOS] + deploymentTarget: + iOS: 9.0 + tvOS: 10.0 + sources: + - path: MySources + inferDestinationFiltersByPath: true + - path: OtherSources + destinationFilters: [iOS] +``` + +Note that the definition of supported destinations can be applied to every type of bundle making everything more easy to manage (app targets, unit tests, UI tests etc). + ### Sources Specifies the source directories for a target. This can either be a single source or a list of sources. Applicable source files, resources, headers, and `.lproj` files will be parsed appropriately. @@ -483,7 +515,9 @@ A source can be provided via a string (the path) or an object of the form: - [ ] **compilerFlags**: **[String]** or **String** - A list of compilerFlags to add to files under this specific path provided as a list or a space delimited string. Defaults to empty. - [ ] **excludes**: **[String]** - A list of [global patterns](https://en.wikipedia.org/wiki/Glob_(programming)) representing the files to exclude. These rules are relative to `path` and _not the directory where `project.yml` resides_. XcodeGen uses Bash 4's Glob behaviors where globstar (**) is enabled. - [ ] **includes**: **[String]** - A list of global patterns in the same format as `excludes` representing the files to include. These rules are relative to `path` and _not the directory where `project.yml` resides_. If **excludes** is present and file conflicts with **includes**, **excludes** will override the **includes** behavior. -- [ ] **createIntermediateGroups**: **Bool** - This overrides the value in [Options](#options) +- [ ] **destinationFilters**: **[[Supported Destinations](#supported-destinations)]** - List of supported platform destinations the files should filter to. Defaults to all supported destinations. +- [ ] **inferDestinationFiltersByPath**: **Bool** - This is a convenience filter that helps you to filter the files if their paths match these patterns `**//*` or `*_.swift`. Note, if you use `destinationFilters` this flag will be ignored. +- [ ] **createIntermediateGroups**: **Bool** - This overrides the value in [Options](#options). - [ ] **optional**: **Bool** - Disable missing path check. Defaults to false. - [ ] **buildPhase**: **String** - This manually sets the build phase this file or files in this directory will be added to, otherwise XcodeGen will guess based on the file extension. Note that `Info.plist` files will never be added to any build phases, no matter what this setting is. Possible values are: - `sources` - Compile Sources phase @@ -519,9 +553,11 @@ targets: MyTarget: sources: MyTargetSource MyOtherTarget: + supportedDestinations: [iOS, tvOS] sources: - MyOtherTargetSource1 - path: MyOtherTargetSource2 + inferDestinationFiltersByPath: true name: MyNewName excludes: - "ios/*.[mh]" @@ -533,6 +569,7 @@ targets: - "-Werror" - "-Wextra" - path: MyOtherTargetSource3 + destinationFilters: [iOS] compilerFlags: "-Werror -Wextra" - path: ModuleMaps buildPhase: @@ -560,10 +597,11 @@ A dependency can be one of a 6 types: - [ ] **embed**: **Bool** - Whether to embed the dependency. Defaults to true for application target and false for non application targets. - [ ] **link**: **Bool** - Whether to link the dependency. Defaults to `true` depending on the type of the dependency and the type of the target (e.g. static libraries will only link to executables by default). -- [ ] **codeSign**: **Bool** - Whether the `codeSignOnCopy` setting is applied when embedding framework. Defaults to true -- [ ] **removeHeaders**: **Bool** - Whether the `removeHeadersOnCopy` setting is applied when embedding the framework. Defaults to true -- [ ] **weak**: **Bool** - Whether the `Weak` setting is applied when linking the framework. Defaults to false -- [ ] **platformFilter**: **String** - This field is specific to Mac Catalyst. It corresponds to the "Platforms" dropdown in the Frameworks & Libraries section of Target settings in Xcode. Available options are: **iOS**, **macOS** and **all**. Defaults is **all** +- [ ] **codeSign**: **Bool** - Whether the `codeSignOnCopy` setting is applied when embedding framework. Defaults to true. +- [ ] **removeHeaders**: **Bool** - Whether the `removeHeadersOnCopy` setting is applied when embedding the framework. Defaults to true. +- [ ] **weak**: **Bool** - Whether the `Weak` setting is applied when linking the framework. Defaults to false. +- [ ] **platformFilter**: **String** - This field is specific to Mac Catalyst. It corresponds to the "Platforms" dropdown in the Frameworks & Libraries section of Target settings in Xcode. Available options are: **iOS**, **macOS** and **all**. Defaults is **all**. +- [ ] **destinationFilters**: **[[Supported Destinations](#supported-destinations)]** - List of supported platform destinations this dependency should filter to. Defaults to all supported destinations. - [ ] **platforms**: **[[Platform](#platform)]** - List of platforms this dependency should apply to. Defaults to all applicable platforms. - **copy** - Copy Files Phase for this dependency. This only applies when `embed` is true. Must be specified as an object with the following fields: - [x] **destination**: **String** - Destination of the Copy Files phase. This can be one of the following values: @@ -613,13 +651,17 @@ projectReferences: path: path/to/FooLib.xcodeproj targets: MyTarget: + supportedDestinations: [iOS, tvOS] dependencies: - target: MyFramework + destinationFilters: [iOS] - target: FooLib/FooTarget - framework: path/to/framework.framework + destinationFilters: [tvOS] - carthage: Result findFrameworks: false linkType: static + destinationFilters: [iOS] - sdk: Contacts.framework - sdk: libc++.tbd - sdk: libz.dylib diff --git a/SettingPresets/Platforms/tvOS.yml b/SettingPresets/Platforms/tvOS.yml index 26c99a9d..40491c07 100644 --- a/SettingPresets/Platforms/tvOS.yml +++ b/SettingPresets/Platforms/tvOS.yml @@ -1,3 +1,3 @@ -TARGETED_DEVICE_FAMILY: 3 LD_RUNPATH_SEARCH_PATHS: ["$(inherited)", "@executable_path/Frameworks"] SDKROOT: appletvos +TARGETED_DEVICE_FAMILY: 3 diff --git a/SettingPresets/SupportedDestinations/iOS.yml b/SettingPresets/SupportedDestinations/iOS.yml new file mode 100644 index 00000000..91bfa0b4 --- /dev/null +++ b/SettingPresets/SupportedDestinations/iOS.yml @@ -0,0 +1,5 @@ +SUPPORTED_PLATFORMS: iphoneos iphonesimulator +TARGETED_DEVICE_FAMILY: '1,2' +SUPPORTS_MACCATALYST: NO +SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: YES +SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD: YES diff --git a/SettingPresets/SupportedDestinations/macCatalyst.yml b/SettingPresets/SupportedDestinations/macCatalyst.yml new file mode 100644 index 00000000..953af268 --- /dev/null +++ b/SettingPresets/SupportedDestinations/macCatalyst.yml @@ -0,0 +1,2 @@ +SUPPORTS_MACCATALYST: YES +SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO diff --git a/SettingPresets/SupportedDestinations/macOS.yml b/SettingPresets/SupportedDestinations/macOS.yml new file mode 100644 index 00000000..e59a8f69 --- /dev/null +++ b/SettingPresets/SupportedDestinations/macOS.yml @@ -0,0 +1,3 @@ +SUPPORTED_PLATFORMS: macosx +SUPPORTS_MACCATALYST: NO +SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO diff --git a/SettingPresets/SupportedDestinations/tvOS.yml b/SettingPresets/SupportedDestinations/tvOS.yml new file mode 100644 index 00000000..a40ce266 --- /dev/null +++ b/SettingPresets/SupportedDestinations/tvOS.yml @@ -0,0 +1,2 @@ +SUPPORTED_PLATFORMS: appletvos appletvsimulator +TARGETED_DEVICE_FAMILY: '3' diff --git a/SettingPresets/SupportedDestinations/visionOS.yml b/SettingPresets/SupportedDestinations/visionOS.yml new file mode 100644 index 00000000..1353d719 --- /dev/null +++ b/SettingPresets/SupportedDestinations/visionOS.yml @@ -0,0 +1,3 @@ +SUPPORTED_PLATFORMS: xros xrsimulator +TARGETED_DEVICE_FAMILY: '7' +SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD: NO diff --git a/Sources/ProjectSpec/Dependency.swift b/Sources/ProjectSpec/Dependency.swift index b102b127..f7a72c0a 100644 --- a/Sources/ProjectSpec/Dependency.swift +++ b/Sources/ProjectSpec/Dependency.swift @@ -16,6 +16,7 @@ public struct Dependency: Equatable { public var implicit: Bool = implicitDefault public var weakLink: Bool = weakLinkDefault public var platformFilter: PlatformFilter = platformFilterDefault + public var destinationFilters: [SupportedDestination]? public var platforms: Set? public var copyPhase: BuildPhaseSpec.CopyFilesSettings? @@ -28,6 +29,7 @@ public struct Dependency: Equatable { implicit: Bool = implicitDefault, weakLink: Bool = weakLinkDefault, platformFilter: PlatformFilter = platformFilterDefault, + destinationFilters: [SupportedDestination]? = nil, platforms: Set? = nil, copyPhase: BuildPhaseSpec.CopyFilesSettings? = nil ) { @@ -39,6 +41,7 @@ public struct Dependency: Equatable { self.implicit = implicit self.weakLink = weakLink self.platformFilter = platformFilter + self.destinationFilters = destinationFilters self.platforms = platforms self.copyPhase = copyPhase } @@ -48,7 +51,7 @@ public struct Dependency: Equatable { case iOS case macOS } - + public enum CarthageLinkType: String { case dynamic case `static` @@ -142,7 +145,11 @@ extension Dependency: JSONObjectConvertible { } else { self.platformFilter = .all } - + + if let destinationFilters: [SupportedDestination] = jsonDictionary.json(atKeyPath: "destinationFilters") { + self.destinationFilters = destinationFilters + } + if let platforms: [ProjectSpec.Platform] = jsonDictionary.json(atKeyPath: "platforms") { self.platforms = Set(platforms) } @@ -161,6 +168,7 @@ extension Dependency: JSONEncodable { "link": link, "platforms": platforms?.map(\.rawValue).sorted(), "copy": copyPhase?.toJSONValue(), + "destinationFilters": destinationFilters?.map { $0.rawValue }, ] if removeHeaders != Dependency.removeHeadersDefault { diff --git a/Sources/ProjectSpec/DeploymentTarget.swift b/Sources/ProjectSpec/DeploymentTarget.swift index d3c9e3f7..a22f25ce 100644 --- a/Sources/ProjectSpec/DeploymentTarget.swift +++ b/Sources/ProjectSpec/DeploymentTarget.swift @@ -26,6 +26,7 @@ public struct DeploymentTarget: Equatable { public func version(for platform: Platform) -> Version? { switch platform { + case .auto: return nil case .iOS: return iOS case .tvOS: return tvOS case .watchOS: return watchOS @@ -39,6 +40,7 @@ extension Platform { public var deploymentTargetSetting: String { switch self { + case .auto: return "" case .iOS: return "IPHONEOS_DEPLOYMENT_TARGET" case .tvOS: return "TVOS_DEPLOYMENT_TARGET" case .watchOS: return "WATCHOS_DEPLOYMENT_TARGET" @@ -49,6 +51,7 @@ extension Platform { public var sdkRoot: String { switch self { + case .auto: return "auto" case .iOS: return "iphoneos" case .tvOS: return "appletvos" case .watchOS: return "watchos" diff --git a/Sources/ProjectSpec/Platform.swift b/Sources/ProjectSpec/Platform.swift index 69a976f1..5f7613b4 100644 --- a/Sources/ProjectSpec/Platform.swift +++ b/Sources/ProjectSpec/Platform.swift @@ -1,9 +1,10 @@ import Foundation public enum Platform: String, Hashable, CaseIterable { + case auto case iOS - case watchOS case tvOS case macOS + case watchOS case visionOS } diff --git a/Sources/ProjectSpec/Plist.swift b/Sources/ProjectSpec/Plist.swift index d478fd01..124b4bfb 100644 --- a/Sources/ProjectSpec/Plist.swift +++ b/Sources/ProjectSpec/Plist.swift @@ -30,7 +30,7 @@ extension Plist: JSONEncodable { [ "path": path, "properties": properties, - ] + ] as [String : Any] } } diff --git a/Sources/ProjectSpec/Settings.swift b/Sources/ProjectSpec/Settings.swift index 6ac0328c..f59006a7 100644 --- a/Sources/ProjectSpec/Settings.swift +++ b/Sources/ProjectSpec/Settings.swift @@ -113,7 +113,7 @@ extension Settings: JSONEncodable { "base": buildSettings, "groups": groups, "configs": configSettings.mapValues { $0.toJSONValue() }, - ] + ] as [String : Any] } return buildSettings } diff --git a/Sources/ProjectSpec/SpecParsingError.swift b/Sources/ProjectSpec/SpecParsingError.swift index 59ae2ad9..b875eadc 100644 --- a/Sources/ProjectSpec/SpecParsingError.swift +++ b/Sources/ProjectSpec/SpecParsingError.swift @@ -7,6 +7,7 @@ public enum SpecParsingError: Error, CustomStringConvertible { case unknownPackageRequirement([String: Any]) case invalidSourceBuildPhase(String) case invalidTargetReference(String) + case invalidTargetPlatformAsArray case invalidVersion(String) case unknownBreakpointType(String) case unknownBreakpointScope(String) @@ -27,6 +28,8 @@ public enum SpecParsingError: Error, CustomStringConvertible { return "Invalid Source Build Phase: \(error)" case let .invalidTargetReference(targetReference): return "Invalid Target Reference Syntax: \(targetReference)" + case .invalidTargetPlatformAsArray: + return "Invalid Target platform: Array not allowed with supported destinations" case let .invalidVersion(version): return "Invalid version: \(version)" case let .unknownPackageRequirement(package): diff --git a/Sources/ProjectSpec/SpecValidation.swift b/Sources/ProjectSpec/SpecValidation.swift index 018a5243..9bf43e1a 100644 --- a/Sources/ProjectSpec/SpecValidation.swift +++ b/Sources/ProjectSpec/SpecValidation.swift @@ -180,6 +180,29 @@ extension Project { errors.append(.invalidTargetSource(target: target.name, source: sourcePath.string)) } } + + if target.supportedDestinations != nil, target.platform == .watchOS { + errors.append(.unexpectedTargetPlatformForSupportedDestinations(target: target.name, platform: target.platform)) + } + + if target.supportedDestinations?.contains(.macOS) == true, + target.supportedDestinations?.contains(.macCatalyst) == true { + + errors.append(.multipleMacPlatformsInSupportedDestinations(target: target.name)) + } + + if target.supportedDestinations?.contains(.macCatalyst) == true, + target.platform != .iOS, target.platform != .auto { + + errors.append(.invalidTargetPlatformForSupportedDestinations(target: target.name)) + } + + if target.platform != .auto, target.platform != .watchOS, + let supportedDestination = SupportedDestination(rawValue: target.platform.rawValue), + target.supportedDestinations?.contains(supportedDestination) == false { + + errors.append(.missingTargetPlatformInSupportedDestinations(target: target.name, platform: target.platform)) + } } for projectReference in projectReferences { diff --git a/Sources/ProjectSpec/SpecValidationError.swift b/Sources/ProjectSpec/SpecValidationError.swift index 639ccb28..6564067a 100644 --- a/Sources/ProjectSpec/SpecValidationError.swift +++ b/Sources/ProjectSpec/SpecValidationError.swift @@ -17,6 +17,10 @@ public struct SpecValidationError: Error, CustomStringConvertible { case invalidTargetConfigFile(target: String, configFile: String, config: String) case invalidTargetSchemeConfigVariant(target: String, configVariant: String, configType: ConfigType) case invalidTargetSchemeTest(target: String, testTarget: String) + case invalidTargetPlatformForSupportedDestinations(target: String) + case unexpectedTargetPlatformForSupportedDestinations(target: String, platform: Platform) + case multipleMacPlatformsInSupportedDestinations(target: String) + case missingTargetPlatformInSupportedDestinations(target: String, platform: Platform) case invalidSchemeTarget(scheme: String, target: String, action: String) case invalidSchemeConfig(scheme: String, config: String) case invalidSwiftPackage(name: String, target: String) @@ -54,6 +58,14 @@ public struct SpecValidationError: Error, CustomStringConvertible { return "Target \(target.quoted) has an invalid scheme config variant which requires a config that has a \(configType.rawValue.quoted) type and contains the name \(configVariant.quoted)" case let .invalidTargetSchemeTest(target, test): return "Target \(target.quoted) scheme has invalid test \(test.quoted)" + case let .invalidTargetPlatformForSupportedDestinations(target): + return "Target \(target.quoted) has supported destinations that require a target platform iOS or auto" + case let .unexpectedTargetPlatformForSupportedDestinations(target, platform): + return "Target \(target.quoted) has platform \(platform.rawValue.quoted) that does not expect supported destinations" + case let .multipleMacPlatformsInSupportedDestinations(target): + return "Target \(target.quoted) has multiple definitions of mac platforms in supported destinations" + case let .missingTargetPlatformInSupportedDestinations(target, platform): + return "Target \(target.quoted) has platform \(platform.rawValue.quoted) that is missing in supported destinations" case let .invalidConfigFile(configFile, config): return "Invalid config file \(configFile.quoted) for config \(config.quoted)" case let .invalidSchemeTarget(scheme, target, action): diff --git a/Sources/ProjectSpec/SupportedDestination.swift b/Sources/ProjectSpec/SupportedDestination.swift new file mode 100644 index 00000000..15a776a4 --- /dev/null +++ b/Sources/ProjectSpec/SupportedDestination.swift @@ -0,0 +1,45 @@ +import Foundation + +public enum SupportedDestination: String, CaseIterable { + case iOS + case tvOS + case macOS + case macCatalyst + case visionOS +} + +extension SupportedDestination { + + public var string: String { + switch self { + case .iOS: + return "ios" + case .tvOS: + return "tvos" + case .macOS: + return "macos" + case .macCatalyst: + return "maccatalyst" + case .visionOS: + return "xros" + } + } + + // This is used to: + // 1. Get the first one and apply SettingPresets 'Platforms' and 'Product_Platform' if the platform is 'auto' + // 2. Sort, loop and merge together SettingPresets 'SupportedDestinations' + public var priority: Int { + switch self { + case .iOS: + return 0 + case .tvOS: + return 1 + case .visionOS: + return 2 + case .macOS: + return 3 + case .macCatalyst: + return 4 + } + } +} diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index e7856ab7..3b9f2c53 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -37,6 +37,7 @@ public struct Target: ProjectTarget { public var name: String public var type: PBXProductType public var platform: Platform + public var supportedDestinations: [SupportedDestination]? public var settings: Settings public var sources: [TargetSource] public var dependencies: [Dependency] @@ -58,7 +59,7 @@ public struct Target: ProjectTarget { public var productName: String public var onlyCopyFilesOnInstall: Bool public var putResourcesBeforeSourcesBuildPhase: Bool - + public var isLegacy: Bool { legacy != nil } @@ -78,6 +79,7 @@ public struct Target: ProjectTarget { name: String, type: PBXProductType, platform: Platform, + supportedDestinations: [SupportedDestination]? = nil, productName: String? = nil, deploymentTarget: Version? = nil, settings: Settings = .empty, @@ -103,6 +105,7 @@ public struct Target: ProjectTarget { self.name = name self.type = type self.platform = platform + self.supportedDestinations = supportedDestinations self.deploymentTarget = deploymentTarget self.productName = productName ?? name self.settings = settings @@ -162,15 +165,17 @@ extension Target { guard let targetsDictionary: [String: JSONDictionary] = jsonDictionary["targets"] as? [String: JSONDictionary] else { return jsonDictionary } - + var crossPlatformTargets: [String: JSONDictionary] = [:] for (targetName, target) in targetsDictionary { - if let platforms = target["platform"] as? [String] { - for platform in platforms { var platformTarget = target + + /// This value is set to help us to check, in Target init, that there are no conflicts in the definition of the platforms. We want to ensure that the user didn't define, at the same time, + /// the new Xcode 14 supported destinations and the XcodeGen generation of Multiple Platform Targets (when you define the platform field as an array). + platformTarget["isMultiPlatformTarget"] = true platformTarget = platformTarget.expand(variables: ["platform": platform]) @@ -202,8 +207,8 @@ extension Target { crossPlatformTargets[targetName] = target } } + var merged = jsonDictionary - merged["targets"] = crossPlatformTargets return merged } @@ -268,19 +273,41 @@ extension Target: NamedJSONDictionaryConvertible { let resolvedName: String = jsonDictionary.json(atKeyPath: "name") ?? name self.name = resolvedName productName = jsonDictionary.json(atKeyPath: "productName") ?? resolvedName - let typeString: String = try jsonDictionary.json(atKeyPath: "type") + + let typeString: String = jsonDictionary.json(atKeyPath: "type") ?? "" if let type = PBXProductType(string: typeString) { self.type = type } else { throw SpecParsingError.unknownTargetType(typeString) } - let platformString: String = try jsonDictionary.json(atKeyPath: "platform") + + if let supportedDestinations: [SupportedDestination] = jsonDictionary.json(atKeyPath: "supportedDestinations") { + self.supportedDestinations = supportedDestinations + } + + let isResolved = jsonDictionary.json(atKeyPath: "isMultiPlatformTarget") ?? false + if isResolved, supportedDestinations != nil { + throw SpecParsingError.invalidTargetPlatformAsArray + } + + var platformString: String = jsonDictionary.json(atKeyPath: "platform") ?? "" + // platform defaults to 'auto' if it is empty and we are using supported destinations + if supportedDestinations != nil, platformString.isEmpty { + platformString = Platform.auto.rawValue + } + // we add 'iOS' in supported destinations if it contains only 'macCatalyst' + if supportedDestinations?.contains(.macCatalyst) == true, + supportedDestinations?.contains(.iOS) == false { + + supportedDestinations?.append(.iOS) + } + if let platform = Platform(rawValue: platformString) { self.platform = platform } else { throw SpecParsingError.unknownTargetPlatform(platformString) } - + if let string: String = jsonDictionary.json(atKeyPath: "deploymentTarget") { deploymentTarget = try Version.parse(string) } else if let double: Double = jsonDictionary.json(atKeyPath: "deploymentTarget") { @@ -351,6 +378,7 @@ extension Target: JSONEncodable { var dict: [String: Any?] = [ "type": type.name, "platform": platform.rawValue, + "supportedDestinations": supportedDestinations?.map { $0.rawValue }, "settings": settings.toJSONValue(), "configFiles": configFiles, "attributes": attributes, diff --git a/Sources/ProjectSpec/TargetSource.swift b/Sources/ProjectSpec/TargetSource.swift index 6674972b..f5932c83 100644 --- a/Sources/ProjectSpec/TargetSource.swift +++ b/Sources/ProjectSpec/TargetSource.swift @@ -23,6 +23,8 @@ public struct TargetSource: Equatable { public var createIntermediateGroups: Bool? public var attributes: [String] public var resourceTags: [String] + public var inferDestinationFiltersByPath: Bool? + public var destinationFilters: [SupportedDestination]? public enum HeaderVisibility: String { case `public` @@ -51,7 +53,9 @@ public struct TargetSource: Equatable { headerVisibility: HeaderVisibility? = nil, createIntermediateGroups: Bool? = nil, attributes: [String] = [], - resourceTags: [String] = [] + resourceTags: [String] = [], + inferDestinationFiltersByPath: Bool? = nil, + destinationFilters: [SupportedDestination]? = nil ) { self.path = (path as NSString).standardizingPath self.name = name @@ -66,6 +70,8 @@ public struct TargetSource: Equatable { self.createIntermediateGroups = createIntermediateGroups self.attributes = attributes self.resourceTags = resourceTags + self.inferDestinationFiltersByPath = inferDestinationFiltersByPath + self.destinationFilters = destinationFilters } } @@ -112,6 +118,12 @@ extension TargetSource: JSONObjectConvertible { createIntermediateGroups = jsonDictionary.json(atKeyPath: "createIntermediateGroups") attributes = jsonDictionary.json(atKeyPath: "attributes") ?? [] resourceTags = jsonDictionary.json(atKeyPath: "resourceTags") ?? [] + + inferDestinationFiltersByPath = jsonDictionary.json(atKeyPath: "inferDestinationFiltersByPath") + + if let destinationFilters: [SupportedDestination] = jsonDictionary.json(atKeyPath: "destinationFilters") { + self.destinationFilters = destinationFilters + } } } @@ -129,6 +141,8 @@ extension TargetSource: JSONEncodable { "createIntermediateGroups": createIntermediateGroups, "resourceTags": resourceTags, "path": path, + "inferDestinationFiltersByPath": inferDestinationFiltersByPath, + "destinationFilters": destinationFilters?.map { $0.rawValue }, ] if optional != TargetSource.optionalDefault { diff --git a/Sources/ProjectSpec/TestPlan.swift b/Sources/ProjectSpec/TestPlan.swift index 541b4e63..c47b17ca 100644 --- a/Sources/ProjectSpec/TestPlan.swift +++ b/Sources/ProjectSpec/TestPlan.swift @@ -25,7 +25,7 @@ extension TestPlan: JSONEncodable { [ "path": path, "defaultPlan": defaultPlan, - ] + ] as [String : Any] } } diff --git a/Sources/ProjectSpec/XCProjExtensions.swift b/Sources/ProjectSpec/XCProjExtensions.swift index bbb3247e..e1da8a3b 100644 --- a/Sources/ProjectSpec/XCProjExtensions.swift +++ b/Sources/ProjectSpec/XCProjExtensions.swift @@ -82,6 +82,7 @@ extension Platform { public var emoji: String { switch self { + case .auto: return "🤖" case .iOS: return "📱" case .watchOS: return "⌚️" case .tvOS: return "📺" diff --git a/Sources/XcodeGenKit/CarthageDependencyResolver.swift b/Sources/XcodeGenKit/CarthageDependencyResolver.swift index 8fcb6e46..46b83ac9 100644 --- a/Sources/XcodeGenKit/CarthageDependencyResolver.swift +++ b/Sources/XcodeGenKit/CarthageDependencyResolver.swift @@ -151,6 +151,9 @@ extension Platform { public var carthageName: String { switch self { + case .auto: + // This is a dummy value + return "auto" case .iOS: return "iOS" case .tvOS: diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 0ac632a8..82e87f2c 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -340,7 +340,7 @@ public class PBXProjGenerator { return addObject(buildConfig) } - var dependencies = target.targets.map { generateTargetDependency(from: target.name, to: $0, platform: nil) } + var dependencies = target.targets.map { generateTargetDependency(from: target.name, to: $0, platform: nil, platforms: nil) } let defaultConfigurationName = project.options.defaultConfig ?? project.configs.first?.name ?? "" let buildConfigList = addObject(XCConfigurationList( @@ -359,7 +359,7 @@ public class PBXProjGenerator { aggregateTarget.dependencies = dependencies } - func generateTargetDependency(from: String, to target: String, platform: String?) -> PBXTargetDependency { + func generateTargetDependency(from: String, to target: String, platform: String?, platforms: [String]?) -> PBXTargetDependency { guard let targetObject = targetObjects[target] ?? targetAggregateObjects[target] else { fatalError("Target dependency not found: from ( \(from) ) to ( \(target) )") } @@ -376,6 +376,7 @@ public class PBXProjGenerator { let targetDependency = addObject( PBXTargetDependency( platformFilter: platform, + platformFilters: platforms, target: targetObject, targetProxy: targetProxy ) @@ -727,7 +728,7 @@ public class PBXProjGenerator { return !linkingAttributes.isEmpty ? ["ATTRIBUTES": linkingAttributes] : nil } - func processTargetDependency(_ dependency: Dependency, dependencyTarget: Target, embedFileReference: PBXFileElement?, platform: String?) { + func processTargetDependency(_ dependency: Dependency, dependencyTarget: Target, embedFileReference: PBXFileElement?, platform: String?, platforms: [String]?) { let dependencyLinkage = dependencyTarget.defaultLinkage let link = dependency.link ?? ((dependencyLinkage == .dynamic && target.type != .staticLibrary) || @@ -736,6 +737,7 @@ public class PBXProjGenerator { if link, let dependencyFile = embedFileReference { let pbxBuildFile = PBXBuildFile(file: dependencyFile, settings: getDependencyFrameworkSettings(dependency: dependency)) pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms let buildFile = addObject(pbxBuildFile) targetFrameworkBuildFiles.append(buildFile) @@ -752,6 +754,7 @@ public class PBXProjGenerator { settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? !dependencyTarget.type.isExecutable) ) pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms let embedFile = addObject(pbxBuildFile) if dependency.copyPhase != nil { @@ -787,6 +790,7 @@ public class PBXProjGenerator { let embed = dependency.embed ?? target.shouldEmbedDependencies let platform = makePlatformFilter(for: dependency.platformFilter) + let platforms = makeDestinationFilters(for: dependency.destinationFilters) switch dependency.type { case .target: @@ -795,15 +799,15 @@ public class PBXProjGenerator { switch dependencyTargetReference.location { case .local: let dependencyTargetName = dependency.reference - let targetDependency = generateTargetDependency(from: target.name, to: dependencyTargetName, platform: platform) + let targetDependency = generateTargetDependency(from: target.name, to: dependencyTargetName, platform: platform, platforms: platforms) dependencies.append(targetDependency) guard let dependencyTarget = project.getTarget(dependencyTargetName) else { continue } - processTargetDependency(dependency, dependencyTarget: dependencyTarget, embedFileReference: targetFileReferences[dependencyTarget.name], platform: platform) + processTargetDependency(dependency, dependencyTarget: dependencyTarget, embedFileReference: targetFileReferences[dependencyTarget.name], platform: platform, platforms: platforms) case .project(let dependencyProjectName): let dependencyTargetName = dependencyTargetReference.name let (targetDependency, dependencyTarget, dependencyProductProxy) = try generateExternalTargetDependency(from: target.name, to: dependencyTargetName, in: dependencyProjectName, platform: target.platform) dependencies.append(targetDependency) - processTargetDependency(dependency, dependencyTarget: dependencyTarget, embedFileReference: dependencyProductProxy, platform: platform) + processTargetDependency(dependency, dependencyTarget: dependencyTarget, embedFileReference: dependencyProductProxy, platform: platform, platforms: platforms) } case .framework: @@ -829,6 +833,7 @@ public class PBXProjGenerator { if dependency.link ?? (target.type != .staticLibrary) { let pbxBuildFile = PBXBuildFile(file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency)) pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms let buildFile = addObject(pbxBuildFile) targetFrameworkBuildFiles.append(buildFile) @@ -841,6 +846,7 @@ public class PBXProjGenerator { if embed { let pbxBuildFile = PBXBuildFile(file: fileReference, settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true)) pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms let embedFile = addObject(pbxBuildFile) if dependency.copyPhase != nil { @@ -891,12 +897,14 @@ public class PBXProjGenerator { settings: getDependencyFrameworkSettings(dependency: dependency) ) pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms let buildFile = addObject(pbxBuildFile) targetFrameworkBuildFiles.append(buildFile) if dependency.embed == true { let pbxBuildFile = PBXBuildFile(file: fileReference, settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true)) pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms let embedFile = addObject(pbxBuildFile) if dependency.copyPhase != nil { @@ -926,6 +934,7 @@ public class PBXProjGenerator { if dependency.link ?? (!isStaticLibrary && !isCarthageStaticLink) { let pbxBuildFile = PBXBuildFile(file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency)) pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms let buildFile = addObject(pbxBuildFile) targetFrameworkBuildFiles.append(buildFile) } @@ -954,11 +963,12 @@ public class PBXProjGenerator { if link { let file = PBXBuildFile(product: packageDependency, settings: getDependencyFrameworkSettings(dependency: dependency)) file.platformFilter = platform + file.platformFilters = platforms let buildFile = addObject(file) targetFrameworkBuildFiles.append(buildFile) } else { let targetDependency = addObject( - PBXTargetDependency(platformFilter: platform, product: packageDependency) + PBXTargetDependency(platformFilter: platform, platformFilters: platforms, product: packageDependency) ) dependencies.append(targetDependency) } @@ -967,6 +977,7 @@ public class PBXProjGenerator { let pbxBuildFile = PBXBuildFile(product: packageDependency, settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true)) pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms let embedFile = addObject(pbxBuildFile) if dependency.copyPhase != nil { @@ -999,6 +1010,7 @@ public class PBXProjGenerator { settings: embed ? getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true) : nil ) pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms let buildFile = addObject(pbxBuildFile) copyBundlesReferences.append(buildFile) @@ -1445,7 +1457,12 @@ public class PBXProjGenerator { return "ios" } } - + + private func makeDestinationFilters(for filters: [SupportedDestination]?) -> [String]? { + guard let filters = filters, !filters.isEmpty else { return nil } + return filters.map { $0.string } + } + /// Make `Build Tools Plug-ins` as a dependency to the target /// - Parameter target: ProjectTarget /// - Returns: Elements for referencing other targets through content proxies. @@ -1466,7 +1483,7 @@ public class PBXProjGenerator { return targetDependency } } - + func getInfoPlists(for target: Target) -> [Config: String] { var searchForDefaultInfoPlist: Bool = true var defaultInfoPlist: String? @@ -1600,7 +1617,7 @@ extension Platform { /// - returns: `true` for platforms that the app store requires simulator slices to be stripped. public var requiresSimulatorStripping: Bool { switch self { - case .iOS, .tvOS, .watchOS, .visionOS: + case .auto, .iOS, .tvOS, .watchOS, .visionOS: return true case .macOS: return false diff --git a/Sources/XcodeGenKit/SettingsBuilder.swift b/Sources/XcodeGenKit/SettingsBuilder.swift index e4ae29f4..69aadf6f 100644 --- a/Sources/XcodeGenKit/SettingsBuilder.swift +++ b/Sources/XcodeGenKit/SettingsBuilder.swift @@ -41,16 +41,63 @@ extension Project { public func getTargetBuildSettings(target: Target, config: Config) -> BuildSettings { var buildSettings = BuildSettings() - + + // list of supported destination sorted by priority + let specSupportedDestinations = target.supportedDestinations?.sorted(by: { $0.priority < $1.priority }) ?? [] + if options.settingPresets.applyTarget { - buildSettings += SettingsPresetFile.platform(target.platform).getBuildSettings() + let platform: Platform + + if target.platform == .auto, + let firstDestination = specSupportedDestinations.first, + let firstDestinationPlatform = Platform(rawValue: firstDestination.rawValue) { + + platform = firstDestinationPlatform + } else { + platform = target.platform + } + + buildSettings += SettingsPresetFile.platform(platform).getBuildSettings() buildSettings += SettingsPresetFile.product(target.type).getBuildSettings() - buildSettings += SettingsPresetFile.productPlatform(target.type, target.platform).getBuildSettings() + buildSettings += SettingsPresetFile.productPlatform(target.type, platform).getBuildSettings() + + if target.platform == .auto { + // this fix is necessary because the platform preset overrides the original value + buildSettings["SDKROOT"] = Platform.auto.rawValue + } } - + + if !specSupportedDestinations.isEmpty { + var supportedPlatforms: [String] = [] + var targetedDeviceFamily: [String] = [] + + for supportedDestination in specSupportedDestinations { + let supportedPlatformBuildSettings = SettingsPresetFile.supportedDestination(supportedDestination).getBuildSettings() + buildSettings += supportedPlatformBuildSettings + + if let value = supportedPlatformBuildSettings?["SUPPORTED_PLATFORMS"] as? String { + supportedPlatforms += value.components(separatedBy: " ") + } + if let value = supportedPlatformBuildSettings?["TARGETED_DEVICE_FAMILY"] as? String { + targetedDeviceFamily += value.components(separatedBy: ",") + } + } + + buildSettings["SUPPORTED_PLATFORMS"] = supportedPlatforms.joined(separator: " ") + buildSettings["TARGETED_DEVICE_FAMILY"] = targetedDeviceFamily.joined(separator: ",") + } + // apply custom platform version if let version = target.deploymentTarget { - buildSettings[target.platform.deploymentTargetSetting] = version.deploymentTarget + if !specSupportedDestinations.isEmpty { + for supportedDestination in specSupportedDestinations { + if let platform = Platform(rawValue: supportedDestination.rawValue) { + buildSettings[platform.deploymentTargetSetting] = version.deploymentTarget + } + } + } else { + buildSettings[target.platform.deploymentTargetSetting] = version.deploymentTarget + } } // Prevent setting presets from overrwriting settings in target xcconfig files @@ -205,7 +252,7 @@ extension SettingsPresetFile { guard let settingsPath = possibleSettingsPaths.first(where: { $0.exists }) else { switch self { - case .base, .config, .platform: + case .base, .config, .platform, .supportedDestination: print("No \"\(name)\" settings found") case .product, .productPlatform: break diff --git a/Sources/XcodeGenKit/SettingsPresetFile.swift b/Sources/XcodeGenKit/SettingsPresetFile.swift index 8cbffad1..30a1f410 100644 --- a/Sources/XcodeGenKit/SettingsPresetFile.swift +++ b/Sources/XcodeGenKit/SettingsPresetFile.swift @@ -5,6 +5,7 @@ import XcodeProj public enum SettingsPresetFile { case config(ConfigType) case platform(Platform) + case supportedDestination(SupportedDestination) case product(PBXProductType) case productPlatform(PBXProductType, Platform) case base @@ -13,6 +14,7 @@ public enum SettingsPresetFile { switch self { case let .config(config): return "Configs/\(config.rawValue)" case let .platform(platform): return "Platforms/\(platform.rawValue)" + case let .supportedDestination(supportedDestination): return "SupportedDestinations/\(supportedDestination.rawValue)" case let .product(product): return "Products/\(product.name)" case let .productPlatform(product, platform): return "Product_Platform/\(product.name)_\(platform.rawValue)" case .base: return "base" @@ -23,6 +25,7 @@ public enum SettingsPresetFile { switch self { case let .config(config): return "\(config.rawValue) config" case let .platform(platform): return platform.rawValue + case let .supportedDestination(supportedDestination): return supportedDestination.rawValue case let .product(product): return product.name case let .productPlatform(product, platform): return "\(platform) \(product)" case .base: return "base" diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 3ffcbb3a..21155156 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -109,7 +109,23 @@ class SourceGenerator { return nil } } - + + private func makeDestinationFilters(for path: Path, with filters: [SupportedDestination]?, or inferDestinationFiltersByPath: Bool?) -> [String]? { + if let filters = filters, !filters.isEmpty { + return filters.map { $0.string } + } else if inferDestinationFiltersByPath == true { + for supportedDestination in SupportedDestination.allCases { + let regex1 = try? NSRegularExpression(pattern: "\\/\(supportedDestination)\\/", options: .caseInsensitive) + let regex2 = try? NSRegularExpression(pattern: "\\_\(supportedDestination)\\.swift$", options: .caseInsensitive) + + if regex1?.isMatch(to: path.string) == true || regex2?.isMatch(to: path.string) == true { + return [supportedDestination.string] + } + } + } + return nil + } + func generateSourceFile(targetType: PBXProductType, targetSource: TargetSource, path: Path, fileReference: PBXFileElement? = nil, buildPhases: [Path: BuildPhaseSpec]) -> SourceFile { let fileReference = fileReference ?? fileReferencesByPath[path.string.lowercased()]! var settings: [String: Any] = [:] @@ -174,8 +190,10 @@ class SourceGenerator { if chosenBuildPhase == .resources && !assetTags.isEmpty { settings["ASSET_TAGS"] = assetTags } - - let buildFile = PBXBuildFile(file: fileReference, settings: settings.isEmpty ? nil : settings) + + let platforms = makeDestinationFilters(for: path, with: targetSource.destinationFilters, or: targetSource.inferDestinationFiltersByPath) + + let buildFile = PBXBuildFile(file: fileReference, settings: settings.isEmpty ? nil : settings, platformFilters: platforms) return SourceFile( path: path, fileReference: fileReference, diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/Info.generated.plist b/Tests/Fixtures/TestProject/App_supportedDestinations/Info.generated.plist new file mode 100644 index 00000000..8bed04c0 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/Info.generated.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + TestApp + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0.0 + + diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/Sources/MyAppApp.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/Sources/MyAppApp.swift new file mode 100644 index 00000000..2db0d884 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/Sources/MyAppApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct MyAppApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/Sources/iOS/ContentView.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/Sources/iOS/ContentView.swift new file mode 100644 index 00000000..1e32a55a --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/Sources/iOS/ContentView.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundColor(.accentColor) + Text("Hello, world! on iOS") + } + .padding() + } +} diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/Sources/tvOS/ContentView.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/Sources/tvOS/ContentView.swift new file mode 100644 index 00000000..f0512afc --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/Sources/tvOS/ContentView.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "house") + .imageScale(.large) + .foregroundColor(.accentColor) + Text("Hello, world! tvOS") + } + .padding() + } +} diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/Storyboards/LaunchScreen.storyboard b/Tests/Fixtures/TestProject/App_supportedDestinations/Storyboards/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/Storyboards/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_MACCATALYST.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_MACCATALYST.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_MACCATALYST.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_ios.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_ios.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_ios.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_macOS.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_macOS.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_macOS.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_tvOs.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_tvOs.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/File_tvOs.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/TVOS/File_B.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/TVOS/File_B.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/TVOS/File_B.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/iOs/File_A.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/iOs/File_A.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/iOs/File_A.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/macCatalyst/File_D.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/macCatalyst/File_D.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/macCatalyst/File_D.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/macos/File_C.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/macos/File_C.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/TestResources/macos/File_C.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj index 0fbe3f3a..5cdebf96 100644 --- a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 0F99AECCB4691803C791CDCE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2FC2A8A829CE71B1CF415FF7 /* Main.storyboard */; }; 15129B8D9ED000BDA1FEEC27 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A2F16890ECF2EE3FED72AE /* AppDelegate.swift */; }; 1551370B0ACAC632E15C853B /* SwiftyJSON.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF47010E7368583405AA50CB /* SwiftyJSON.framework */; }; + 1B485D6584C3B47AC58831C6 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18722C61B05FFF4CC63D5755 /* ContentView.swift */; platformFilters = (tvos, ); }; 1BC891D89980D82738D963F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 74FBDFA5CB063F6001AD8ACD /* Main.storyboard */; }; 1E03FC7312293997599C6435 /* Empty.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 068EDF47F0B087F6A4052AC0 /* Empty.h */; }; 1E2A4D61E96521FF7123D7B0 /* XPC Service.xpc in CopyFiles */ = {isa = PBXBuildFile; fileRef = 22237B8EBD9E6BE8EBC8735F /* XPC Service.xpc */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -56,6 +57,7 @@ 21CA04F29CD0DEB0DD27B808 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C5AC2545AE4D4F7F44E2E9B /* Result.framework */; }; 262891CCD5F74316610437FA /* Framework2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3EF21DF245F66BEF5446AAEF /* Framework2.framework */; platformFilter = ios; settings = {ATTRIBUTES = (Weak, ); }; }; 265B6A05C0198FD2EB485173 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C033648A37D95027845BD3 /* main.swift */; }; + 2698ED273D0A5820B28CAD20 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D52EC9AA9FFD3B690C355068 /* LaunchScreen.storyboard */; platformFilters = (ios, ); }; 2730C6D0A35AED4ADD6EDF17 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0704B6CAFBB53E0EBB08F6B3 /* ViewController.swift */; settings = {COMPILER_FLAGS = "-Werror"; }; }; 28A96EBC76D53817AABDA91C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 8AF20308873AEEEC4D8C45D1 /* Settings.bundle */; }; 2A5356FCC03EE312F1738C61 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09B82F603D981398F38D762E /* AppDelegate.swift */; }; @@ -70,6 +72,7 @@ 339578307B9266AB3D7722D9 /* File2.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC56891DA7446EAC8C2F27EB /* File2.swift */; }; 3535891EC86283BB5064E7E1 /* Headers in Headers */ = {isa = PBXBuildFile; fileRef = 2E1E747C7BC434ADB80CC269 /* Headers */; settings = {ATTRIBUTES = (Public, ); }; }; 3788E1382B38DF4ACE3D2BB1 /* MyFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A58A16491CDDF968B0D56DE /* MyFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3A3BA9F91994D8B472C71F04 /* Swinject in Frameworks */ = {isa = PBXBuildFile; platformFilters = (tvos, ); productRef = C7F9B7EDE85527EFEA85D46D /* Swinject */; }; 3BBCA6F76E5F212E9C55FB78 /* BundleX.bundle in Copy Bundle Resources */ = {isa = PBXBuildFile; fileRef = 45C12576F5AA694DD0CE2132 /* BundleX.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3C5134EE524310ACF7B7CD6E /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CAF6C55B555E3E1352645B6 /* ExtensionDelegate.swift */; }; 3DF22C477446669094AC7C8C /* ExternalTarget.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F6ADE654A3459AFDA2CC0CD3 /* ExternalTarget.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -148,6 +151,7 @@ B18C121B0A4D43ED8149D8E2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 79325B44B19B83EC6CEDBCC5 /* LaunchScreen.storyboard */; }; B20617116B230DED1F7AF5E5 /* libStaticLibrary_ObjC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B221F5A689AD7D3AD52F56B8 /* libStaticLibrary_ObjC.a */; }; B2D43A31C184E34EF9CB743C /* Framework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8A9274BE42A03DC5DA1FAD04 /* Framework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B358AB913543E62237FCC4E3 /* MyAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A4363D659A58DA835DE8BA /* MyAppApp.swift */; }; B47F2629BFE5853767C8BB5E /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDB2B6A77D39CD5602F2125F /* Contacts.framework */; }; B49D3A51787E362DE4D0E78A /* SomeXPCService.xpc in CopyFiles */ = {isa = PBXBuildFile; fileRef = 70A8E15C81E454DC950C59F0 /* SomeXPCService.xpc */; }; B502EF8F7605CBD038298F23 /* CrossOverlayFramework.swiftcrossimport in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8DD7A61B07AD2F91BDECC255 /* CrossOverlayFramework.swiftcrossimport */; }; @@ -163,6 +167,7 @@ C836F09B677937EFF69B1FCE /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C934C1F7A68CCD0AB6B38478 /* NotificationController.swift */; }; C88598A49087A212990F4E8B /* ResourceFolder in Resources */ = {isa = PBXBuildFile; fileRef = 6B1603BA83AA0C7B94E45168 /* ResourceFolder */; }; CAE18A2194B57C830A297F83 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6680EFE4E908CDBDCE405C8 /* main.swift */; }; + CAF8470C7F1BF207DBE6AEE3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEEFDE76B5FEC833403C0869 /* ContentView.swift */; platformFilters = (ios, ); }; CCA17097382757012B58C17C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1BC32A813B80A53962A1F365 /* Assets.xcassets */; }; D058D241BDF5FB0C919BBECA /* CrossOverlayFramework.swiftcrossimport in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8DD7A61B07AD2F91BDECC255 /* CrossOverlayFramework.swiftcrossimport */; }; D5458D67C3596943114C3205 /* Standalone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5BD97AF0F94A15A5B7DDB7 /* Standalone.swift */; }; @@ -738,8 +743,10 @@ 108BB29172D27BE3BD1E7F35 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 13EEAB58665D79C15184D9D0 /* App_iOS_UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = App_iOS_UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 148B7C933698BCC4F1DBA979 /* XPC_Service.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XPC_Service.m; sourceTree = ""; }; + 15A4363D659A58DA835DE8BA /* MyAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppApp.swift; sourceTree = ""; }; 16AA52945B70B1BF9E246350 /* FilterDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterDataProvider.swift; sourceTree = ""; }; 16D662EE577E4CD6AFF39D66 /* config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = config.xcconfig; sourceTree = ""; }; + 18722C61B05FFF4CC63D5755 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 187E665975BB5611AF0F27E1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 1BC32A813B80A53962A1F365 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1D0C79A8C750EC0DE748C463 /* StaticLibrary_ObjC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StaticLibrary_ObjC.m; sourceTree = ""; }; @@ -766,6 +773,7 @@ 3ED831531AA349CCC19B258B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 3EF21DF245F66BEF5446AAEF /* Framework2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Framework2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3FC04772130400920D68A167 /* App_Clip_Tests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = App_Clip_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 407C3F0009FDCE5B1B7DC2A8 /* App_supportedDestinations.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = App_supportedDestinations.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40863AE6202CFCD0529D8438 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 41FC82ED1C4C3B7B3D7B2FB7 /* Framework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Framework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 432E2C071A4B6B3757BEA13E /* Driver.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Driver.cpp; sourceTree = ""; }; @@ -830,6 +838,7 @@ AAA49985DFFE797EE8416887 /* inputList.xcfilelist */ = {isa = PBXFileReference; lastKnownFileType = text.xcfilelist; path = inputList.xcfilelist; sourceTree = ""; }; AB055761199DF36DB0C629A6 /* Framework2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Framework2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AEBCA8CFF769189C0D52031E /* App_iOS.xctestplan */ = {isa = PBXFileReference; path = App_iOS.xctestplan; sourceTree = ""; }; + AEEFDE76B5FEC833403C0869 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; B17B8D9C9B391332CD176A35 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LocalizedStoryboard.storyboard; sourceTree = ""; }; B198242976C3395E31FE000A /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; B1C33BB070583BE3B0EC0E68 /* App_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = App_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -855,6 +864,7 @@ D21BB1B6FA5A025305B223BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; D296BB7355994040E197A1EE /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = ""; }; D51CC8BCCBD68A90E90A3207 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D52EC9AA9FFD3B690C355068 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; D629E142AB87C681D4EC90F7 /* iMessageExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = iMessageExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D6C89D80B5458D8929F5C127 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; D70BE0C05E5779A077793BE6 /* Model 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 2.xcdatamodel"; sourceTree = ""; }; @@ -909,6 +919,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 36418B6CABA06BA9B206556E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A3BA9F91994D8B472C71F04 /* Swinject in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5EFF61D0A49AA8EABD72DF44 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1102,6 +1120,7 @@ 0D039F2E62354C7C8E283BE6 /* App_iOS_UITests */, EE78B4FBD0137D1975C47D76 /* App_macOS */, 6DE1C805DC13547F27FD86C6 /* App_macOS_Tests */, + D27117FBA2920408002F0B4C /* App_supportedDestinations */, BAE6C12745737019DC9E98BF /* App_watchOS */, 795B8D70B674C850B57DD39D /* App_watchOS Extension */, 6DBE0EE90642BB3F6E58AD43 /* Configs */, @@ -1194,6 +1213,23 @@ path = UnderFileGroup; sourceTree = ""; }; + 69C61547C081D04364A5DE42 /* Storyboards */ = { + isa = PBXGroup; + children = ( + D52EC9AA9FFD3B690C355068 /* LaunchScreen.storyboard */, + ); + name = Storyboards; + path = Storyboards; + sourceTree = ""; + }; + 6A90AFD865B13D26DA108CAB /* tvOS */ = { + isa = PBXGroup; + children = ( + 18722C61B05FFF4CC63D5755 /* ContentView.swift */, + ); + path = tvOS; + sourceTree = ""; + }; 6BD8F0932CCAD4BBE752866B /* App_Clip_UITests */ = { isa = PBXGroup; children = ( @@ -1266,6 +1302,17 @@ path = "XPC Service"; sourceTree = ""; }; + 85FEBB7D2103B020423407A2 /* Sources */ = { + isa = PBXGroup; + children = ( + 9E5249A284275B0CDF4E5DDA /* iOS */, + 6A90AFD865B13D26DA108CAB /* tvOS */, + 15A4363D659A58DA835DE8BA /* MyAppApp.swift */, + ); + name = Sources; + path = Sources; + sourceTree = ""; + }; 8CFD8AD4820FAB9265663F92 /* Tool */ = { isa = PBXGroup; children = ( @@ -1301,6 +1348,14 @@ path = StandaloneFiles; sourceTree = ""; }; + 9E5249A284275B0CDF4E5DDA /* iOS */ = { + isa = PBXGroup; + children = ( + AEEFDE76B5FEC833403C0869 /* ContentView.swift */, + ); + path = iOS; + sourceTree = ""; + }; 9EDF27BB8A57733E6639D36D /* Resources */ = { isa = PBXGroup; children = ( @@ -1343,6 +1398,7 @@ B1C33BB070583BE3B0EC0E68 /* App_iOS.app */, 0B9D98D935F2C69A1F5BA539 /* App_macOS_Tests.xctest */, 33F6DCDC37D2E66543D4965D /* App_macOS.app */, + 407C3F0009FDCE5B1B7DC2A8 /* App_supportedDestinations.app */, 0D09D243DBCF9D32E239F1E8 /* App_watchOS Extension.appex */, A680BE9F68A255B0FB291AE6 /* App_watchOS.app */, 84317819C92F78425870E483 /* BundleX.bundle */, @@ -1432,6 +1488,15 @@ path = StaticLibrary_Swift; sourceTree = ""; }; + D27117FBA2920408002F0B4C /* App_supportedDestinations */ = { + isa = PBXGroup; + children = ( + 85FEBB7D2103B020423407A2 /* Sources */, + 69C61547C081D04364A5DE42 /* Storyboards */, + ); + path = App_supportedDestinations; + sourceTree = ""; + }; D557819B1EE5B42A0A3DD4D1 /* tvOS */ = { isa = PBXGroup; children = ( @@ -2205,6 +2270,26 @@ productReference = 7D700FA699849D2F95216883 /* EntitledApp.app */; productType = "com.apple.product-type.application"; }; + C0570E2FB50D830D8D423396 /* App_supportedDestinations */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5EC789CCE1928A4CDA00DD1E /* Build configuration list for PBXNativeTarget "App_supportedDestinations" */; + buildPhases = ( + CF8BAE171BAAFB2E5DDB9C19 /* Sources */, + 97119CD9F01D9A9522EF3526 /* Resources */, + 36418B6CABA06BA9B206556E /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = App_supportedDestinations; + packageProductDependencies = ( + C7F9B7EDE85527EFEA85D46D /* Swinject */, + ); + productName = App_supportedDestinations; + productReference = 407C3F0009FDCE5B1B7DC2A8 /* App_supportedDestinations.app */; + productType = "com.apple.product-type.application"; + }; C7F90FD0FAAF232B3E015D38 /* CrossOverlayFramework_watchOS */ = { isa = PBXNativeTarget; buildConfigurationList = C483BD5456B09C276DE6EFC1 /* Build configuration list for PBXNativeTarget "CrossOverlayFramework_watchOS" */; @@ -2415,6 +2500,7 @@ F674B2CFC4738EEC49BAD0DA /* App_iOS_UITests */, 020A320BB3736FCDE6CC4E70 /* App_macOS */, 71E2BDAC4B8E8FC2BBF75C55 /* App_macOS_Tests */, + C0570E2FB50D830D8D423396 /* App_supportedDestinations */, 208179651927D1138D19B5AD /* App_watchOS */, 307AE3FA155FFD09B74AE351 /* App_watchOS Extension */, DA40AB367B606CCE2FDD398D /* BundleX */, @@ -2520,6 +2606,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 97119CD9F01D9A9522EF3526 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2698ED273D0A5820B28CAD20 /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; AA12B5909FEE45016F469C78 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3000,6 +3094,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CF8BAE171BAAFB2E5DDB9C19 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CAF8470C7F1BF207DBE6AEE3 /* ContentView.swift in Sources */, + 1B485D6584C3B47AC58831C6 /* ContentView.swift in Sources */, + B358AB913543E62237FCC4E3 /* MyAppApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D1F422E9C4DD531AA88418C9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -4108,6 +4212,26 @@ }; name = "Staging Debug"; }; + 24CFBB3ABB9E14E85035B892 /* Production Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = App_supportedDestinations/Info.generated.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.test.TestApp; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2,3"; + }; + name = "Production Debug"; + }; 2569D399CA3C4828EF87AD78 /* Test Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -4815,6 +4939,26 @@ }; name = "Production Release"; }; + 4C11A3E7EA48B5FB556D4614 /* Staging Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = App_supportedDestinations/Info.generated.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.test.TestApp; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2,3"; + }; + name = "Staging Debug"; + }; 4D5DC2028DC046B8AF0B9B83 /* Test Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5558,6 +5702,26 @@ }; name = "Staging Debug"; }; + 75B3C83C754AA9C12ABF5E54 /* Staging Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = App_supportedDestinations/Info.generated.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.test.TestApp; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2,3"; + }; + name = "Staging Release"; + }; 77B8B41EBA5D778EB3AF89DC /* Production Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5757,6 +5921,26 @@ }; name = "Production Release"; }; + 7F3F6A813F8B126258780E8D /* Production Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = App_supportedDestinations/Info.generated.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.test.TestApp; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2,3"; + }; + name = "Production Release"; + }; 7F86E00770E76CA3412A03BD /* Staging Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -6288,6 +6472,26 @@ }; name = "Staging Debug"; }; + 98EE00BF46FAE62F16C25E9C /* Test Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = App_supportedDestinations/Info.generated.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.test.TestApp; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2,3"; + }; + name = "Test Release"; + }; 9A891313A139893990989BDD /* Test Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -6469,6 +6673,26 @@ }; name = "Production Debug"; }; + A4015127D0A01FE60CF5621B /* Test Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = App_supportedDestinations/Info.generated.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.test.TestApp; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2,3"; + }; + name = "Test Debug"; + }; A59DDFBFCF18C44A993CFB00 /* Test Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -8721,6 +8945,19 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = "Production Debug"; }; + 5EC789CCE1928A4CDA00DD1E /* Build configuration list for PBXNativeTarget "App_supportedDestinations" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 24CFBB3ABB9E14E85035B892 /* Production Debug */, + 7F3F6A813F8B126258780E8D /* Production Release */, + 4C11A3E7EA48B5FB556D4614 /* Staging Debug */, + 75B3C83C754AA9C12ABF5E54 /* Staging Release */, + A4015127D0A01FE60CF5621B /* Test Debug */, + 98EE00BF46FAE62F16C25E9C /* Test Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Production Debug"; + }; 62C52A55CB8D3BD9A055FD14 /* Build configuration list for PBXNativeTarget "App_macOS_Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -9125,6 +9362,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + C7F9B7EDE85527EFEA85D46D /* Swinject */ = { + isa = XCSwiftPackageProductDependency; + package = 4EDA79334592CBBA0E507AD2 /* XCRemoteSwiftPackageReference "Swinject" */; + productName = Swinject; + }; D7917D10F77DA9D69937D493 /* Swinject */ = { isa = XCSwiftPackageProductDependency; package = 4EDA79334592CBBA0E507AD2 /* XCRemoteSwiftPackageReference "Swinject" */; diff --git a/Tests/Fixtures/TestProject/project.yml b/Tests/Fixtures/TestProject/project.yml index f6666784..dcef8e1e 100644 --- a/Tests/Fixtures/TestProject/project.yml +++ b/Tests/Fixtures/TestProject/project.yml @@ -232,6 +232,29 @@ targets: properties: com.apple.security.application-groups: group.com.app + App_supportedDestinations: + type: application + supportedDestinations: [iOS, tvOS] + info: + path: App_supportedDestinations/Info.generated.plist + properties: + CFBundleDisplayName: "TestApp" + CFBundleVersion: "1.0.0" + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.test.TestApp + sources: + - path: App_supportedDestinations/Sources + group: App_supportedDestinations + inferDestinationFiltersByPath: true + - path: App_supportedDestinations/Storyboards + group: App_supportedDestinations + destinationFilters: [iOS] + dependencies: + - package: Swinject + product: Swinject + destinationFilters: [tvOS] + App_watchOS: type: application.watchapp2 platform: watchOS @@ -482,7 +505,7 @@ schemes: enableGPUFrameCaptureMode: "disabled" enableGPUValidationMode: "disabled" storeKitConfiguration: "App_iOS/Configuration.storekit" - macroExpansion: App_iOS + macroExpansion: App_iOS test: gatherCoverageData: true targets: diff --git a/Tests/ProjectSpecTests/ProjectSpecTests.swift b/Tests/ProjectSpecTests/ProjectSpecTests.swift index 644e9fbc..5f1f4854 100644 --- a/Tests/ProjectSpecTests/ProjectSpecTests.swift +++ b/Tests/ProjectSpecTests/ProjectSpecTests.swift @@ -73,12 +73,23 @@ class ProjectSpecTests: XCTestCase { describe { $0.it("has correct build setting") { + try expect(Platform.auto.deploymentTargetSetting) == "" try expect(Platform.iOS.deploymentTargetSetting) == "IPHONEOS_DEPLOYMENT_TARGET" try expect(Platform.tvOS.deploymentTargetSetting) == "TVOS_DEPLOYMENT_TARGET" try expect(Platform.watchOS.deploymentTargetSetting) == "WATCHOS_DEPLOYMENT_TARGET" try expect(Platform.macOS.deploymentTargetSetting) == "MACOSX_DEPLOYMENT_TARGET" + try expect(Platform.visionOS.deploymentTargetSetting) == "XROS_DEPLOYMENT_TARGET" } + $0.it("has correct sdk root") { + try expect(Platform.auto.sdkRoot) == "auto" + try expect(Platform.iOS.sdkRoot) == "iphoneos" + try expect(Platform.tvOS.sdkRoot) == "appletvos" + try expect(Platform.watchOS.sdkRoot) == "watchos" + try expect(Platform.macOS.sdkRoot) == "macosx" + try expect(Platform.visionOS.sdkRoot) == "xros" + } + $0.it("parses version correctly") { try expect(Version.parse("2").deploymentTarget) == "2.0" try expect(Version.parse("2.0").deploymentTarget) == "2.0" @@ -178,7 +189,59 @@ class ProjectSpecTests: XCTestCase { try expectNoValidationError(project, .duplicateDependencies(target: "target1", dependencyReference: "package1")) } - + + $0.it("unexpected supported destinations for watch app") { + var project = baseProject + project.targets = [ + Target( + name: "target1", + type: .application, + platform: .watchOS, + supportedDestinations: [.macOS] + ) + ] + try expectValidationError(project, .unexpectedTargetPlatformForSupportedDestinations(target: "target1", platform: .watchOS)) + } + + $0.it("multiple definitions of mac platform in supported destinations") { + var project = baseProject + project.targets = [ + Target( + name: "target1", + type: .application, + platform: .iOS, + supportedDestinations: [.macOS, .macCatalyst] + ) + ] + try expectValidationError(project, .multipleMacPlatformsInSupportedDestinations(target: "target1")) + } + + $0.it("invalid target platform for macCatalyst supported destinations") { + var project = baseProject + project.targets = [ + Target( + name: "target1", + type: .application, + platform: .tvOS, + supportedDestinations: [.tvOS, .macCatalyst] + ) + ] + try expectValidationError(project, .invalidTargetPlatformForSupportedDestinations(target: "target1")) + } + + $0.it("missing target platform in supported destinations") { + var project = baseProject + project.targets = [ + Target( + name: "target1", + type: .application, + platform: .iOS, + supportedDestinations: [.tvOS] + ) + ] + try expectValidationError(project, .missingTargetPlatformInSupportedDestinations(target: "target1", platform: .iOS)) + } + $0.it("allows non-existent configurations") { var project = baseProject project.options = SpecOptions(disabledValidations: [.missingConfigs]) diff --git a/Tests/ProjectSpecTests/SpecLoadingTests.swift b/Tests/ProjectSpecTests/SpecLoadingTests.swift index 2fe22184..6a81c090 100644 --- a/Tests/ProjectSpecTests/SpecLoadingTests.swift +++ b/Tests/ProjectSpecTests/SpecLoadingTests.swift @@ -604,10 +604,66 @@ class SpecLoadingTests: XCTestCase { target_iOS.deploymentTarget = Version(major: 9, minor: 0, patch: 0) target_tvOS.deploymentTarget = Version(major: 10, minor: 0, patch: 0) - try expect(project.targets.count) == 2 try expect(project.targets) == [target_iOS, target_tvOS] } + + $0.it("parses no platform fallbacks to auto if we are using supported destinations") { + let targetDictionary: [String: Any] = [ + "type": "framework", + "supportedDestinations": ["iOS", "tvOS"] + ] + let project = try getProjectSpec(["targets": ["Framework": targetDictionary]]) + let target = Target(name: "Framework", type: .framework, platform: .auto) + + try expect(project.targets) == [target] + } + + $0.it("parses no platform fails if we are not using supported destinations") { + let expectedError = SpecParsingError.unknownTargetPlatform("") + + let projectDictionary: [String: Any] = [ + "name": "test", + "targets": ["target1": [ + "type": "application" + ] as [String : Any]] + ] + + try expectError(expectedError) { + _ = try Project(jsonDictionary: projectDictionary) + } + } + + $0.it("parses supported destinations with macCatalyst but not iOS, we add iOS") { + let targetDictionary: [String: Any] = [ + "type": "framework", + "supportedDestinations": ["macCatalyst"] + ] + + let project = try getProjectSpec(["targets": ["Framework": targetDictionary]]) + let target = Target(name: "Framework", type: .framework, platform: .auto) + + try expect(project.targets) == [target] + try expect(project.targets.first?.supportedDestinations) == [.macCatalyst, .iOS] + } + + $0.it("invalid target platform because platform is an array and supported destinations is in use") { + let expectedError = SpecParsingError.invalidTargetPlatformAsArray + + let projectDictionary: [String: Any] = [ + "name": "test", + "targets": ["target1": [ + "type": "application", + "platform": ["iOS", "tvOS"], + "supportedDestinations": ["iOS", "tvOS"] + ] as [String : Any]] + ] + + try expectError(expectedError) { + _ = try Project(jsonDictionary: projectDictionary) + } + } + $0.it("parses target templates") { let targetDictionary: [String: Any] = [ diff --git a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift index 703126c7..28dbd21a 100644 --- a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift @@ -335,7 +335,156 @@ class ProjectGeneratorTests: XCTestCase { try expect(targetConfig.buildSettings["WATCHOS_DEPLOYMENT_TARGET"] as? String) == "2.0" try expect(targetConfig.buildSettings["TVOS_DEPLOYMENT_TARGET"]).beNil() } - + + $0.it("supportedPlaforms merges settings - iOS, tvOS") { + let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.tvOS, .iOS]) + let project = Project(name: "", targets: [target]) + + let pbxProject = try project.generatePbxProj() + let targetConfig1 = try unwrap(pbxProject.nativeTargets.first?.buildConfigurationList?.buildConfigurations.first) + + try expect(targetConfig1.buildSettings["SUPPORTED_PLATFORMS"] as? String) == "iphoneos iphonesimulator appletvos appletvsimulator" + try expect(targetConfig1.buildSettings["TARGETED_DEVICE_FAMILY"] as? String) == "1,2,3" + try expect(targetConfig1.buildSettings["SUPPORTS_MACCATALYST"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == true + try expect(targetConfig1.buildSettings["SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == true + + try expect(targetConfig1.buildSettings["LD_RUNPATH_SEARCH_PATHS"] as? [String]) == ["$(inherited)", "@executable_path/Frameworks"] + try expect(targetConfig1.buildSettings["SDKROOT"] as? String) == "auto" + try expect(targetConfig1.buildSettings["ASSETCATALOG_COMPILER_APPICON_NAME"] as? String) == "AppIcon" + try expect(targetConfig1.buildSettings["CODE_SIGN_IDENTITY"] as? String) == "iPhone Developer" + } + + $0.it("supportedPlaforms merges settings - iOS, visionOS") { + let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.visionOS, .iOS]) + let project = Project(name: "", targets: [target]) + + let pbxProject = try project.generatePbxProj() + let targetConfig1 = try unwrap(pbxProject.nativeTargets.first?.buildConfigurationList?.buildConfigurations.first) + + try expect(targetConfig1.buildSettings["SUPPORTED_PLATFORMS"] as? String) == "iphoneos iphonesimulator xros xrsimulator" + try expect(targetConfig1.buildSettings["TARGETED_DEVICE_FAMILY"] as? String) == "1,2,7" + try expect(targetConfig1.buildSettings["SUPPORTS_MACCATALYST"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == true + try expect(targetConfig1.buildSettings["SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == false + + try expect(targetConfig1.buildSettings["LD_RUNPATH_SEARCH_PATHS"] as? [String]) == ["$(inherited)", "@executable_path/Frameworks"] + try expect(targetConfig1.buildSettings["SDKROOT"] as? String) == "auto" + try expect(targetConfig1.buildSettings["ASSETCATALOG_COMPILER_APPICON_NAME"] as? String) == "AppIcon" + try expect(targetConfig1.buildSettings["CODE_SIGN_IDENTITY"] as? String) == "iPhone Developer" + } + + $0.it("supportedPlaforms merges settings - iOS, tvOS, macOS") { + let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.iOS, .tvOS, .macOS]) + let project = Project(name: "", targets: [target]) + + let pbxProject = try project.generatePbxProj() + let targetConfig1 = try unwrap(pbxProject.nativeTargets.first?.buildConfigurationList?.buildConfigurations.first) + + try expect(targetConfig1.buildSettings["SUPPORTED_PLATFORMS"] as? String) == "iphoneos iphonesimulator appletvos appletvsimulator macosx" + try expect(targetConfig1.buildSettings["TARGETED_DEVICE_FAMILY"] as? String) == "1,2,3" + try expect(targetConfig1.buildSettings["SUPPORTS_MACCATALYST"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == true + + try expect(targetConfig1.buildSettings["LD_RUNPATH_SEARCH_PATHS"] as? [String]) == ["$(inherited)", "@executable_path/Frameworks"] + try expect(targetConfig1.buildSettings["SDKROOT"] as? String) == "auto" + try expect(targetConfig1.buildSettings["ASSETCATALOG_COMPILER_APPICON_NAME"] as? String) == "AppIcon" + try expect(targetConfig1.buildSettings["CODE_SIGN_IDENTITY"] as? String) == "iPhone Developer" + } + + $0.it("supportedPlaforms merges settings - iOS, tvOS, macCatalyst") { + let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.iOS, .tvOS, .macCatalyst]) + let project = Project(name: "", targets: [target]) + + let pbxProject = try project.generatePbxProj() + let targetConfig1 = try unwrap(pbxProject.nativeTargets.first?.buildConfigurationList?.buildConfigurations.first) + + try expect(targetConfig1.buildSettings["SUPPORTED_PLATFORMS"] as? String) == "iphoneos iphonesimulator appletvos appletvsimulator" + try expect(targetConfig1.buildSettings["TARGETED_DEVICE_FAMILY"] as? String) == "1,2,3" + try expect(targetConfig1.buildSettings["SUPPORTS_MACCATALYST"] as? Bool) == true + try expect(targetConfig1.buildSettings["SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == true + + try expect(targetConfig1.buildSettings["LD_RUNPATH_SEARCH_PATHS"] as? [String]) == ["$(inherited)", "@executable_path/Frameworks"] + try expect(targetConfig1.buildSettings["SDKROOT"] as? String) == "auto" + try expect(targetConfig1.buildSettings["ASSETCATALOG_COMPILER_APPICON_NAME"] as? String) == "AppIcon" + try expect(targetConfig1.buildSettings["CODE_SIGN_IDENTITY"] as? String) == "iPhone Developer" + } + + $0.it("supportedPlaforms merges settings - iOS, macOS") { + let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.iOS, .macOS]) + let project = Project(name: "", targets: [target]) + + let pbxProject = try project.generatePbxProj() + let targetConfig1 = try unwrap(pbxProject.nativeTargets.first?.buildConfigurationList?.buildConfigurations.first) + + try expect(targetConfig1.buildSettings["SUPPORTED_PLATFORMS"] as? String) == "iphoneos iphonesimulator macosx" + try expect(targetConfig1.buildSettings["TARGETED_DEVICE_FAMILY"] as? String) == "1,2" + try expect(targetConfig1.buildSettings["SUPPORTS_MACCATALYST"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == true + + try expect(targetConfig1.buildSettings["LD_RUNPATH_SEARCH_PATHS"] as? [String]) == ["$(inherited)", "@executable_path/Frameworks"] + try expect(targetConfig1.buildSettings["SDKROOT"] as? String) == "auto" + try expect(targetConfig1.buildSettings["ASSETCATALOG_COMPILER_APPICON_NAME"] as? String) == "AppIcon" + try expect(targetConfig1.buildSettings["CODE_SIGN_IDENTITY"] as? String) == "iPhone Developer" + } + + $0.it("supportedPlaforms merges settings - tvOS, macOS") { + let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.tvOS, .macOS]) + let project = Project(name: "", targets: [target]) + + let pbxProject = try project.generatePbxProj() + let targetConfig1 = try unwrap(pbxProject.nativeTargets.first?.buildConfigurationList?.buildConfigurations.first) + + try expect(targetConfig1.buildSettings["SUPPORTED_PLATFORMS"] as? String) == "appletvos appletvsimulator macosx" + try expect(targetConfig1.buildSettings["TARGETED_DEVICE_FAMILY"] as? String) == "3" + try expect(targetConfig1.buildSettings["SUPPORTS_MACCATALYST"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == false + + try expect(targetConfig1.buildSettings["LD_RUNPATH_SEARCH_PATHS"] as? [String]) == ["$(inherited)", "@executable_path/Frameworks"] + try expect(targetConfig1.buildSettings["SDKROOT"] as? String) == "auto" + try expect(targetConfig1.buildSettings["ASSETCATALOG_COMPILER_APPICON_NAME"] as? String) == "App Icon & Top Shelf Image" + try expect(targetConfig1.buildSettings["ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME"] as? String) == "LaunchImage" + } + + $0.it("supportedPlaforms merges settings - visionOS, macOS") { + let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.visionOS, .macOS]) + let project = Project(name: "", targets: [target]) + + let pbxProject = try project.generatePbxProj() + let targetConfig1 = try unwrap(pbxProject.nativeTargets.first?.buildConfigurationList?.buildConfigurations.first) + + try expect(targetConfig1.buildSettings["SUPPORTED_PLATFORMS"] as? String) == "xros xrsimulator macosx" + try expect(targetConfig1.buildSettings["TARGETED_DEVICE_FAMILY"] as? String) == "7" + try expect(targetConfig1.buildSettings["SUPPORTS_MACCATALYST"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == false + + try expect(targetConfig1.buildSettings["SDKROOT"] as? String) == "auto" + try expect(targetConfig1.buildSettings["ASSETCATALOG_COMPILER_APPICON_NAME"] as? String) == "AppIcon" + } + + $0.it("supportedPlaforms merges settings - iOS, macCatalyst") { + let target = Target(name: "Target", type: .application, platform: .auto, supportedDestinations: [.iOS, .macCatalyst]) + let project = Project(name: "", targets: [target]) + + let pbxProject = try project.generatePbxProj() + let targetConfig1 = try unwrap(pbxProject.nativeTargets.first?.buildConfigurationList?.buildConfigurations.first) + + try expect(targetConfig1.buildSettings["SUPPORTED_PLATFORMS"] as? String) == "iphoneos iphonesimulator" + try expect(targetConfig1.buildSettings["TARGETED_DEVICE_FAMILY"] as? String) == "1,2" + try expect(targetConfig1.buildSettings["SUPPORTS_MACCATALYST"] as? Bool) == true + try expect(targetConfig1.buildSettings["SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == false + try expect(targetConfig1.buildSettings["SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD"] as? Bool) == true + + try expect(targetConfig1.buildSettings["LD_RUNPATH_SEARCH_PATHS"] as? [String]) == ["$(inherited)", "@executable_path/Frameworks"] + try expect(targetConfig1.buildSettings["SDKROOT"] as? String) == "auto" + try expect(targetConfig1.buildSettings["ASSETCATALOG_COMPILER_APPICON_NAME"] as? String) == "AppIcon" + try expect(targetConfig1.buildSettings["CODE_SIGN_IDENTITY"] as? String) == "iPhone Developer" + } + $0.it("generates dependencies") { let pbxProject = try project.generatePbxProj() @@ -1066,7 +1215,108 @@ class ProjectGeneratorTests: XCTestCase { try expect(app2OtherLinkerSettings.contains("-ObjC")) == false try expect(app3OtherLinkerSettings.contains("-ObjC")) == true } - + + $0.it("filter sources with inferDestinationFiltersByPath") { + let sourceFiles = TargetSource(path: "App_supportedDestinations/TestResources", inferDestinationFiltersByPath: true) + + let target = Target( + name: "test", + type: .application, + platform: .auto, + sources: [sourceFiles], + dependencies: [] + ) + + let project = Project( + basePath: fixturePath + "TestProject", + name: "test", + targets: [target] + ) + + let pbxProject = try project.generatePbxProj() + let buildFiles = pbxProject.buildFiles + + try expect(buildFiles.count) == 8 + + for buildFile in buildFiles { + let name = buildFile.file?.nameOrPath + + if buildFile.platformFilters == [SupportedDestination.iOS.string] && + (name == "File_ios.swift" || name == "File_A.swift") { + continue + } else if buildFile.platformFilters == [SupportedDestination.tvOS.string] && + (name == "File_tvOs.swift" || name == "File_B.swift") { + continue + } else if buildFile.platformFilters == [SupportedDestination.macOS.string] && + (name == "File_macOS.swift" || name == "File_C.swift") { + continue + } else if buildFile.platformFilters == [SupportedDestination.macCatalyst.string] && + (name == "File_MACCATALYST.swift" || name == "File_D.swift") { + continue + } + + throw failure("Unexpected source file / destinationFilters") + } + } + + $0.it("filter sources with destinationFilters") { + let sourceFile1 = TargetSource(path: "App_supportedDestinations/TestResources/iOs", + destinationFilters: [.iOS]) + let sourceFile2 = TargetSource(path: "App_supportedDestinations/TestResources/TVOS", + destinationFilters: [.tvOS]) + let sourceFile3 = TargetSource(path: "App_supportedDestinations/TestResources/macos", + destinationFilters: [.macOS, .macCatalyst]) + let sourceFile4 = TargetSource(path: "App_supportedDestinations/TestResources/macCatalyst", + destinationFilters: [.macOS, .macCatalyst]) + let sourceFile5 = TargetSource(path: "App_supportedDestinations/TestResources/File_ios.swift", + destinationFilters: [.iOS]) + let sourceFile6 = TargetSource(path: "App_supportedDestinations/TestResources/File_tvOs.swift", + destinationFilters: [.tvOS]) + let sourceFile7 = TargetSource(path: "App_supportedDestinations/TestResources/File_macOS.swift", + destinationFilters: [.macOS, .macCatalyst]) + let sourceFile8 = TargetSource(path: "App_supportedDestinations/TestResources/File_MACCATALYST.swift", + destinationFilters: [.macOS, .macCatalyst]) + + let target = Target( + name: "test", + type: .application, + platform: .auto, + sources: [sourceFile1, sourceFile2, sourceFile3, sourceFile4, sourceFile5, sourceFile6, sourceFile7, sourceFile8], + dependencies: [] + ) + + let project = Project( + basePath: fixturePath + "TestProject", + name: "test", + targets: [target] + ) + + let pbxProject = try project.generatePbxProj() + let buildFiles = pbxProject.buildFiles + + try expect(buildFiles.count) == 8 + + for buildFile in buildFiles { + let name = buildFile.file?.nameOrPath + + if buildFile.platformFilters == [SupportedDestination.iOS.string] && + (name == "File_ios.swift" || name == "File_A.swift") { + continue + } else if buildFile.platformFilters == [SupportedDestination.tvOS.string] && + (name == "File_tvOs.swift" || name == "File_B.swift") { + continue + } else if buildFile.platformFilters == [SupportedDestination.macOS.string, SupportedDestination.macCatalyst.string] && + (name == "File_C.swift" || name == "File_D.swift") { + continue + } else if buildFile.platformFilters == [SupportedDestination.macOS.string, SupportedDestination.macCatalyst.string] && + (name == "File_macOS.swift" || name == "File_MACCATALYST.swift") { + continue + } + + throw failure("Unexpected source file / destinationFilters") + } + } + $0.it("copies Swift Objective-C Interface Header") { let swiftStaticLibraryWithHeader = Target( name: "swiftStaticLibraryWithHeader", @@ -1645,7 +1895,197 @@ class ProjectGeneratorTests: XCTestCase { } } } + + func testGenerateXcodeProjectWithPlatformFilteredDependencies() throws { + describe("generateXcodeProject with destinationFilters") { + + func generateProjectForApp(withDependencies: [Dependency], targets: [Target], packages: [String: SwiftPackage] = [:]) throws -> PBXProj { + + let app = Target( + name: "App", + type: .application, + platform: .iOS, + dependencies: withDependencies + ) + + let project = Project( + name: "test", + targets: targets + [app], + packages: packages + ) + + return try project.generatePbxProj() + } + + func expectLinkedDependecies(_ expectedLinkedFiles: [String: [String]], in project: PBXProj) throws { + let buildPhases = project.buildPhases + let frameworkPhases = project.frameworksBuildPhases.filter { buildPhases.contains($0) } + + var linkedFiles: [String: [String]] = [:] + + for link in frameworkPhases[0].files ?? [] { + if let name = link.file?.nameOrPath ?? link.product?.productName { + linkedFiles[name] = link.platformFilters + } + } + + try expect(linkedFiles) == expectedLinkedFiles + } + + func expectCopiedBundles(_ expectedCopiedBundleFiles: [String: [String]], in project: PBXProj) throws { + let buildPhases = project.buildPhases + let copyBundlesPhase = project.copyFilesBuildPhases.filter { buildPhases.contains($0) } + + var copiedFiles: [String: [String]] = [:] + + for copy in copyBundlesPhase[0].files ?? [] { + if let name = copy.file?.nameOrPath { + copiedFiles[name] = copy.platformFilters + } + } + + try expect(copiedFiles) == expectedCopiedBundleFiles + } + + $0.it("target dependencies") { + + let frameworkA = Target( + name: "frameworkA", + type: .framework, + platform: .iOS + ) + + let frameworkB = Target( + name: "frameworkB", + type: .framework, + platform: .iOS + ) + + let expectedLinkedFiles = [ + "frameworkA.framework": [SupportedDestination.iOS.string], + "frameworkB.framework": [SupportedDestination.iOS.string, SupportedDestination.tvOS.string] + ] + + // given + let dependencies = [ + Dependency(type: .target, reference: frameworkA.name, destinationFilters: [.iOS]), + Dependency(type: .target, reference: frameworkB.name, destinationFilters: [.iOS, .tvOS]), + ] + + // when + let pbxProject = try generateProjectForApp(withDependencies: dependencies, targets: [frameworkA, frameworkB]) + + // then ensure that everything is linked + try expectLinkedDependecies(expectedLinkedFiles, in: pbxProject) + } + + $0.it("framework dependencies") { + + let expectedLinkedFiles = [ + "frameworkA.framework": [SupportedDestination.iOS.string], + "frameworkB.framework": [SupportedDestination.iOS.string, SupportedDestination.tvOS.string] + ] + + // given + let dependencies = [ + Dependency(type: .framework, reference: "frameworkA.framework", destinationFilters: [.iOS]), + Dependency(type: .framework, reference: "frameworkB.framework", destinationFilters: [.iOS, .tvOS]), + ] + + // when + let pbxProject = try generateProjectForApp(withDependencies: dependencies, targets: []) + + // then ensure that everything is linked + try expectLinkedDependecies(expectedLinkedFiles, in: pbxProject) + } + + $0.it("carthage dependencies") { + + let expectedLinkedFiles = [ + "frameworkA.framework": [SupportedDestination.iOS.string], + "frameworkB.framework": [SupportedDestination.iOS.string, SupportedDestination.tvOS.string] + ] + + // given + let dependencies = [ + Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: "frameworkA.framework", destinationFilters: [.iOS]), + Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: "frameworkB.framework", destinationFilters: [.iOS, .tvOS]), + ] + + // when + let pbxProject = try generateProjectForApp(withDependencies: dependencies, targets: []) + + // then ensure that everything is linked + try expectLinkedDependecies(expectedLinkedFiles, in: pbxProject) + } + + $0.it("sdk dependencies") { + + let expectedLinkedFiles = [ + "sdkA.framework": [SupportedDestination.iOS.string], + "sdkB.framework": [SupportedDestination.iOS.string, SupportedDestination.tvOS.string] + ] + + // given + let dependencies = [ + Dependency(type: .sdk(root: nil), reference: "sdkA.framework", destinationFilters: [.iOS]), + Dependency(type: .sdk(root: nil), reference: "sdkB.framework", destinationFilters: [.iOS, .tvOS]), + ] + + // when + let pbxProject = try generateProjectForApp(withDependencies: dependencies, targets: []) + + // then ensure that everything is linked + try expectLinkedDependecies(expectedLinkedFiles, in: pbxProject) + } + + $0.it("package dependencies") { + + let packages: [String: SwiftPackage] = [ + "RxSwift": .remote(url: "https://github.com/ReactiveX/RxSwift", versionRequirement: .upToNextMajorVersion("5.1.1")), + ] + + let expectedLinkedFiles = [ + "RxSwift": [SupportedDestination.iOS.string], + "RxCocoa": [SupportedDestination.iOS.string, SupportedDestination.tvOS.string] + ] + + // given + let dependencies = [ + Dependency(type: .package(products: ["RxSwift"]), reference: "RxSwift", destinationFilters: [.iOS]), + Dependency(type: .package(products: ["RxCocoa"]), reference: "RxSwift", destinationFilters: [.iOS, .tvOS]), + ] + + // when + let pbxProject = try generateProjectForApp(withDependencies: dependencies, targets: [], packages: packages) + + // then ensure that everything is linked + try expectLinkedDependecies(expectedLinkedFiles, in: pbxProject) + } + + $0.it("bundle dependencies") { + + let expectedCopiedBundleFiles = [ + "bundleA.bundle": [SupportedDestination.iOS.string], + "bundleB.bundle": [SupportedDestination.iOS.string, SupportedDestination.tvOS.string] + ] + + // given + let dependencies = [ + Dependency(type: .bundle, reference: "bundleA.bundle", destinationFilters: [.iOS]), + Dependency(type: .bundle, reference: "bundleB.bundle", destinationFilters: [.iOS, .tvOS]), + ] + + // when + let pbxProject = try generateProjectForApp(withDependencies: dependencies, targets: []) + + // then ensure that everything is linked + try expectCopiedBundles(expectedCopiedBundleFiles, in: pbxProject) + } + } + } + func testGenerateXcodeProjectWithCustomDependencyDestinations() throws { describe("generateXcodeProject") {