diff --git a/Package.swift b/Package.swift index 6319ee25bed..9164916f17a 100644 --- a/Package.swift +++ b/Package.swift @@ -92,6 +92,7 @@ let package = Package( platforms: [ .macOS(.v13), .iOS(.v16), + .macCatalyst(.v17), ], products: autoProducts.flatMap { @@ -119,6 +120,12 @@ let package = Package( type: .dynamic, targets: ["PackageDescription", "CompilerPluginSupport"] ), + .library( + name: "AppleProductTypes", + type: .dynamic, + targets: ["AppleProductTypes"] + ), + .library( name: "PackagePlugin", type: .dynamic, @@ -153,6 +160,21 @@ let package = Package( linkerSettings: packageLibraryLinkSettings ), + // The `AppleProductTypes` target provides additional product types + // to `Package.swift` manifests. Here we build a debug version of the + // library; the bootstrap scripts build the deployable version. + .target( + name: "AppleProductTypes", + // Note: We use `-module-link-name` so clients link against the + // AppleProductTypes library when they import it without further + // messing with the manifest loader. + dependencies: ["PackageDescription"], + swiftSettings: [ + .unsafeFlags(["-package-description-version", "999.0"]), + .unsafeFlags(["-enable-library-evolution"], .when(platforms: [.macOS])), + .unsafeFlags(["-Xfrontend", "-module-link-name", "-Xfrontend", "AppleProductTypes"]) + ]), + // The `PackagePlugin` target provides the API that is available to // plugin scripts. Here we build a debug version of the library; the // bootstrap scripts build the deployable version. @@ -1022,6 +1044,13 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { ] } +/// If ENABLE_APPLE_PRODUCT_TYPES is set in the environment, then also define ENABLE_APPLE_PRODUCT_TYPES in each of the regular targets and test targets. +if ProcessInfo.processInfo.environment["ENABLE_APPLE_PRODUCT_TYPES"] == "1" { + for target in package.targets.filter({ $0.type == .regular || $0.type == .test }) { + target.swiftSettings = (target.swiftSettings ?? []) + [ .define("ENABLE_APPLE_PRODUCT_TYPES") ] + } +} + if ProcessInfo.processInfo.environment["SWIFTPM_SWBUILD_FRAMEWORK"] == nil && ProcessInfo.processInfo.environment["SWIFTPM_NO_SWBUILD_DEPENDENCY"] == nil { diff --git a/Sources/AppleProductTypes/Product.swift b/Sources/AppleProductTypes/Product.swift new file mode 100644 index 00000000000..23f52a0a850 --- /dev/null +++ b/Sources/AppleProductTypes/Product.swift @@ -0,0 +1,90 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +@_spi(PackageProductSettings) import PackageDescription + +#if ENABLE_APPLE_PRODUCT_TYPES +extension Product { + /// Creates an iOS application package product. + /// + /// - Parameters: + /// - name: The name of the application product. + /// - targets: The targets to include in the application product; one and only one of them should be an executable target. + /// - settings: The settings that define the core properties of the application. + public static func iOSApplication( + name: String, + targets: [String], + bundleIdentifier: String? = nil, + teamIdentifier: String? = nil, + displayVersion: String? = nil, + bundleVersion: String? = nil, + iconAssetName: String? = nil, + accentColorAssetName: String? = nil, + supportedDeviceFamilies: [ProductSetting.IOSAppInfo.DeviceFamily], + supportedInterfaceOrientations: [ProductSetting.IOSAppInfo.InterfaceOrientation], + capabilities: [ProductSetting.IOSAppInfo.Capability] = [], + additionalInfoPlistContentFilePath: String? = nil + ) -> Product { + return iOSApplication( + name: name, + targets: targets, + bundleIdentifier: bundleIdentifier, + teamIdentifier: teamIdentifier, + displayVersion: displayVersion, + bundleVersion: bundleVersion, + appIcon: iconAssetName.map({ .asset($0) }), + accentColor: accentColorAssetName.map({ .asset($0) }), + supportedDeviceFamilies: supportedDeviceFamilies, + supportedInterfaceOrientations: supportedInterfaceOrientations, + capabilities: capabilities, + additionalInfoPlistContentFilePath: additionalInfoPlistContentFilePath + ) + } + + /// Creates an iOS application package product. + /// + /// - Parameters: + /// - name: The name of the application product. + /// - targets: The targets to include in the application product; one and only one of them should be an executable target. + /// - settings: The settings that define the core properties of the application. + @available(_PackageDescription, introduced: 5.6) + public static func iOSApplication( + name: String, + targets: [String], + bundleIdentifier: String? = nil, + teamIdentifier: String? = nil, + displayVersion: String? = nil, + bundleVersion: String? = nil, + appIcon: ProductSetting.IOSAppInfo.AppIcon? = nil, + accentColor: ProductSetting.IOSAppInfo.AccentColor? = nil, + supportedDeviceFamilies: [ProductSetting.IOSAppInfo.DeviceFamily], + supportedInterfaceOrientations: [ProductSetting.IOSAppInfo.InterfaceOrientation], + capabilities: [ProductSetting.IOSAppInfo.Capability] = [], + appCategory: ProductSetting.IOSAppInfo.AppCategory? = nil, + additionalInfoPlistContentFilePath: String? = nil + ) -> Product { + return .executable(name: name, targets: targets, settings: [ + bundleIdentifier.map{ .bundleIdentifier($0) }, + teamIdentifier.map{ .teamIdentifier($0) }, + displayVersion.map{ .displayVersion($0) }, + bundleVersion.map{ .bundleVersion($0) }, + .iOSAppInfo(ProductSetting.IOSAppInfo( + appIcon: appIcon, + accentColor: accentColor, + supportedDeviceFamilies: supportedDeviceFamilies, + supportedInterfaceOrientations: supportedInterfaceOrientations, + capabilities: capabilities, + appCategory: appCategory, + additionalInfoPlistContentFilePath: additionalInfoPlistContentFilePath + )) + ].compactMap{ $0 }) + } +} +#endif diff --git a/Sources/PackageDescription/PackageDescriptionSerialization.swift b/Sources/PackageDescription/PackageDescriptionSerialization.swift index 7909faca1de..8ade7137333 100644 --- a/Sources/PackageDescription/PackageDescriptionSerialization.swift +++ b/Sources/PackageDescription/PackageDescriptionSerialization.swift @@ -263,6 +263,10 @@ enum Serialization { let name: String let targets: [String] let productType: ProductType + + #if ENABLE_APPLE_PRODUCT_TYPES + let settings: [ProductSetting] + #endif } // MARK: - trait serialization @@ -301,3 +305,125 @@ enum Serialization { let cxxLanguageStandard: CXXLanguageStandard? } } + +#if ENABLE_APPLE_PRODUCT_TYPES +extension Serialization { + enum ProductSetting: Codable { + case bundleIdentifier(String) + case teamIdentifier(String) + case displayVersion(String) + case bundleVersion(String) + case iOSAppInfo(IOSAppInfo) + + struct IOSAppInfo: Codable { + var appIcon: AppIcon? + var accentColor: AccentColor? + var supportedDeviceFamilies: [DeviceFamily] + var supportedInterfaceOrientations: [InterfaceOrientation] + var capabilities: [Capability] = [] + var appCategory: AppCategory? + var additionalInfoPlistContentFilePath: String? + + enum AccentColor: Codable { + struct PresetColor: Codable { + var rawValue: String + } + + case presetColor(PresetColor) + case asset(String) + } + + enum AppIcon: Codable { + struct PlaceholderIcon: Codable { + var rawValue: String + } + + case placeholder(icon: PlaceholderIcon) + case asset(String) + } + + enum DeviceFamily: String, Codable { + case phone + case pad + case mac + } + + struct DeviceFamilyCondition: Codable { + var deviceFamilies: [DeviceFamily] + } + + enum InterfaceOrientation: Codable { + case portrait(_ condition: DeviceFamilyCondition? = nil) + case portraitUpsideDown(_ condition: DeviceFamilyCondition? = nil) + case landscapeRight(_ condition: DeviceFamilyCondition? = nil) + case landscapeLeft(_ condition: DeviceFamilyCondition? = nil) + } + + enum Capability: Codable { + case appTransportSecurity(configuration: AppTransportSecurityConfiguration, _ condition: DeviceFamilyCondition? = nil) + case bluetoothAlways(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case calendars(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case camera(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case contacts(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case faceID(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case fileAccess(_ location: FileAccessLocation, mode: FileAccessMode, _ condition: DeviceFamilyCondition? = nil) + case incomingNetworkConnections(_ condition: DeviceFamilyCondition? = nil) + case localNetwork(purposeString: String, bonjourServiceTypes: [String]? = nil, _ condition: DeviceFamilyCondition? = nil) + case locationAlwaysAndWhenInUse(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case locationWhenInUse(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case mediaLibrary(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case microphone(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case motion(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case nearbyInteractionAllowOnce(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case outgoingNetworkConnections(_ condition: DeviceFamilyCondition? = nil) + case photoLibrary(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case photoLibraryAdd(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case reminders(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case speechRecognition(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case userTracking(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + } + + struct AppTransportSecurityConfiguration: Codable { + var allowsArbitraryLoadsInWebContent: Bool? = nil + var allowsArbitraryLoadsForMedia: Bool? = nil + var allowsLocalNetworking: Bool? = nil + var exceptionDomains: [ExceptionDomain]? = nil + var pinnedDomains: [PinnedDomain]? = nil + + struct ExceptionDomain: Codable { + var domainName: String + var includesSubdomains: Bool? = nil + var exceptionAllowsInsecureHTTPLoads: Bool? = nil + var exceptionMinimumTLSVersion: String? = nil + var exceptionRequiresForwardSecrecy: Bool? = nil + var requiresCertificateTransparency: Bool? = nil + } + + struct PinnedDomain: Codable { + var domainName: String + var includesSubdomains : Bool? = nil + var pinnedCAIdentities : [[String: String]]? = nil + var pinnedLeafIdentities : [[String: String]]? = nil + } + } + + enum FileAccessLocation: String, Codable { + case userSelectedFiles + case downloadsFolder + case pictureFolder + case musicFolder + case moviesFolder + } + + enum FileAccessMode: String, Codable { + case readOnly + case readWrite + } + + struct AppCategory: Codable { + var rawValue: String + } + } + } +} +#endif diff --git a/Sources/PackageDescription/PackageDescriptionSerializationConversion.swift b/Sources/PackageDescription/PackageDescriptionSerializationConversion.swift index 06b8747b492..09d4f73ebf3 100644 --- a/Sources/PackageDescription/PackageDescriptionSerializationConversion.swift +++ b/Sources/PackageDescription/PackageDescriptionSerializationConversion.swift @@ -362,6 +362,9 @@ extension Serialization.Product { self.name = executable.name self.targets = executable.targets self.productType = .executable + #if ENABLE_APPLE_PRODUCT_TYPES + self.settings = executable.settings.map { .init($0) } + #endif } init(_ library: PackageDescription.Product.Library) { @@ -369,12 +372,18 @@ extension Serialization.Product { self.targets = library.targets let libraryType = library.type.map { ProductType.LibraryType($0) } ?? .automatic self.productType = .library(type: libraryType) + #if ENABLE_APPLE_PRODUCT_TYPES + self.settings = [] + #endif } init(_ plugin: PackageDescription.Product.Plugin) { self.name = plugin.name self.targets = plugin.targets self.productType = .plugin + #if ENABLE_APPLE_PRODUCT_TYPES + self.settings = [] + #endif } } @@ -419,3 +428,213 @@ extension Serialization.SystemPackageProvider { } } } + +#if ENABLE_APPLE_PRODUCT_TYPES +extension Serialization.ProductSetting { + init(_ setting: PackageDescription.ProductSetting) { + switch setting { + case .bundleIdentifier(let value): + self = .bundleIdentifier(value) + case .teamIdentifier(let value): + self = .teamIdentifier(value) + case .displayVersion(let value): + self = .displayVersion(value) + case .bundleVersion(let value): + self = .bundleVersion(value) + case .iOSAppInfo(let appInfo): + self = .iOSAppInfo(.init(appInfo)) + } + } +} + +extension Serialization.ProductSetting.IOSAppInfo { + init(_ appInfo: PackageDescription.ProductSetting.IOSAppInfo) { + self.init( + appIcon: appInfo.appIcon.map { .init($0) }, + accentColor: appInfo.accentColor.map { .init($0) }, + supportedDeviceFamilies: appInfo.supportedDeviceFamilies.map { .init($0) }, + supportedInterfaceOrientations: appInfo.supportedInterfaceOrientations.map { .init($0) }, + capabilities: appInfo.capabilities.map { .init($0) }, + appCategory: appInfo.appCategory.map { .init($0) }, + additionalInfoPlistContentFilePath: appInfo.additionalInfoPlistContentFilePath + ) + } +} + +extension Serialization.ProductSetting.IOSAppInfo.AccentColor { + init(_ color: PackageDescription.ProductSetting.IOSAppInfo.AccentColor) { + switch color { + case .presetColor(let color): + self = .presetColor(.init(color)) + case .asset(let value): + self = .asset(value) + } + } +} + +extension Serialization.ProductSetting.IOSAppInfo.AccentColor.PresetColor { + init(_ color: PackageDescription.ProductSetting.IOSAppInfo.AccentColor.PresetColor) { + self.rawValue = color.rawValue + } +} + +extension Serialization.ProductSetting.IOSAppInfo.AppIcon { + init(_ icon: PackageDescription.ProductSetting.IOSAppInfo.AppIcon) { + switch icon { + case .placeholder(icon: let icon): + self = .placeholder(icon: .init(icon)) + case .asset(let value): + self = .asset(value) + } + } +} + +extension Serialization.ProductSetting.IOSAppInfo.AppIcon.PlaceholderIcon { + init(_ icon: PackageDescription.ProductSetting.IOSAppInfo.AppIcon.PlaceholderIcon) { + self.rawValue = icon.rawValue + } +} + +extension Serialization.ProductSetting.IOSAppInfo.DeviceFamily { + init(_ deviceFamily: PackageDescription.ProductSetting.IOSAppInfo.DeviceFamily) { + switch deviceFamily { + case .phone: self = .phone + case .pad: self = .pad + case .mac: self = .mac + } + } +} + +extension Serialization.ProductSetting.IOSAppInfo.DeviceFamilyCondition { + init(_ condition: PackageDescription.ProductSetting.IOSAppInfo.DeviceFamilyCondition) { + self.init(deviceFamilies: condition.deviceFamilies.map { .init($0) }) + } +} + +extension Serialization.ProductSetting.IOSAppInfo.InterfaceOrientation { + init(_ interfaceOrientation: PackageDescription.ProductSetting.IOSAppInfo.InterfaceOrientation) { + switch interfaceOrientation { + case .portrait(let condition): + self = .portrait(condition.map { .init($0) }) + case .portraitUpsideDown(let condition): + self = .portraitUpsideDown(condition.map { .init($0) }) + case .landscapeRight(let condition): + self = .landscapeRight(condition.map { .init($0) }) + case .landscapeLeft(let condition): + self = .landscapeLeft(condition.map { .init($0) }) + } + } +} + +extension Serialization.ProductSetting.IOSAppInfo.Capability { + init(_ capability: PackageDescription.ProductSetting.IOSAppInfo.Capability) { + switch capability { + case .appTransportSecurity(configuration: let configuration, let condition): + self = .appTransportSecurity(configuration: .init(configuration), condition.map { .init($0) }) + case .bluetoothAlways(purposeString: let purposeString, let condition): + self = .bluetoothAlways(purposeString: purposeString, condition.map { .init($0) }) + case .calendars(purposeString: let purposeString, let condition): + self = .calendars(purposeString: purposeString, condition.map { .init($0) }) + case .camera(purposeString: let purposeString, let condition): + self = .camera(purposeString: purposeString, condition.map { .init($0) }) + case .contacts(purposeString: let purposeString, let condition): + self = .contacts(purposeString: purposeString, condition.map { .init($0) }) + case .faceID(purposeString: let purposeString, let condition): + self = .faceID(purposeString: purposeString, condition.map { .init($0) }) + case .fileAccess(let location, let mode, let condition): + self = .fileAccess(.init(location), mode: .init(mode), condition.map { .init($0) }) + case .incomingNetworkConnections(let condition): + self = .incomingNetworkConnections(condition.map { .init($0) }) + case .localNetwork(purposeString: let purposeString, bonjourServiceTypes: let bonjourServiceTypes, let condition): + self = .localNetwork(purposeString: purposeString, bonjourServiceTypes: bonjourServiceTypes, condition.map { .init($0) }) + case .locationAlwaysAndWhenInUse(purposeString: let purposeString, let condition): + self = .locationAlwaysAndWhenInUse(purposeString: purposeString, condition.map { .init($0) }) + case .locationWhenInUse(purposeString: let purposeString, let condition): + self = .locationWhenInUse(purposeString: purposeString, condition.map { .init($0) }) + case .mediaLibrary(purposeString: let purposeString, let condition): + self = .mediaLibrary(purposeString: purposeString, condition.map { .init($0) }) + case .microphone(purposeString: let purposeString, let condition): + self = .microphone(purposeString: purposeString, condition.map { .init($0) }) + case .motion(purposeString: let purposeString, let condition): + self = .motion(purposeString: purposeString, condition.map { .init($0) }) + case .nearbyInteractionAllowOnce(purposeString: let purposeString, let condition): + self = .nearbyInteractionAllowOnce(purposeString: purposeString, condition.map { .init($0) }) + case .outgoingNetworkConnections(let condition): + self = .outgoingNetworkConnections(condition.map { .init($0) }) + case .photoLibrary(purposeString: let purposeString, let condition): + self = .photoLibrary(purposeString: purposeString, condition.map { .init($0) }) + case .photoLibraryAdd(purposeString: let purposeString, let condition): + self = .photoLibraryAdd(purposeString: purposeString, condition.map { .init($0) }) + case .reminders(purposeString: let purposeString, let condition): + self = .reminders(purposeString: purposeString, condition.map { .init($0) }) + case .speechRecognition(purposeString: let purposeString, let condition): + self = .speechRecognition(purposeString: purposeString, condition.map { .init($0) }) + case .userTracking(purposeString: let purposeString, let condition): + self = .userTracking(purposeString: purposeString, condition.map { .init($0) }) + } + } +} + +extension Serialization.ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration { + init(_ configuration: PackageDescription.ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration) { + self.init( + allowsArbitraryLoadsInWebContent: configuration.allowsArbitraryLoadsInWebContent, + allowsArbitraryLoadsForMedia: configuration.allowsArbitraryLoadsForMedia, + allowsLocalNetworking: configuration.allowsLocalNetworking, + exceptionDomains: configuration.exceptionDomains?.map { .init($0) }, + pinnedDomains: configuration.pinnedDomains?.map { .init($0) } + ) + } +} + +extension Serialization.ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.ExceptionDomain { + init(_ exceptionDomain: PackageDescription.ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.ExceptionDomain) { + self.init( + domainName: exceptionDomain.domainName, + includesSubdomains: exceptionDomain.includesSubdomains, + exceptionAllowsInsecureHTTPLoads: exceptionDomain.exceptionAllowsInsecureHTTPLoads, + exceptionMinimumTLSVersion: exceptionDomain.exceptionMinimumTLSVersion, + exceptionRequiresForwardSecrecy: exceptionDomain.exceptionRequiresForwardSecrecy, + requiresCertificateTransparency: exceptionDomain.requiresCertificateTransparency + ) + } +} + +extension Serialization.ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.PinnedDomain { + init(_ pinnedDomain: PackageDescription.ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.PinnedDomain) { + self.init( + domainName: pinnedDomain.domainName, + includesSubdomains: pinnedDomain.includesSubdomains, + pinnedCAIdentities: pinnedDomain.pinnedCAIdentities, + pinnedLeafIdentities: pinnedDomain.pinnedLeafIdentities + ) + } +} + +extension Serialization.ProductSetting.IOSAppInfo.FileAccessLocation { + init(_ fileAccessLocation: PackageDescription.ProductSetting.IOSAppInfo.FileAccessLocation) { + switch fileAccessLocation { + case .userSelectedFiles: self = .userSelectedFiles + case .downloadsFolder: self = .downloadsFolder + case .pictureFolder: self = .pictureFolder + case .musicFolder: self = .musicFolder + case .moviesFolder: self = .moviesFolder + } + } +} + +extension Serialization.ProductSetting.IOSAppInfo.FileAccessMode { + init(_ fileAccessNode: PackageDescription.ProductSetting.IOSAppInfo.FileAccessMode) { + switch fileAccessNode { + case .readOnly: self = .readOnly + case .readWrite: self = .readWrite + } + } +} + +extension Serialization.ProductSetting.IOSAppInfo.AppCategory { + init(_ appCategory: PackageDescription.ProductSetting.IOSAppInfo.AppCategory) { + self.rawValue = appCategory.rawValue + } +} +#endif diff --git a/Sources/PackageDescription/Product.swift b/Sources/PackageDescription/Product.swift index 73a533c5203..e3b6a7352c4 100644 --- a/Sources/PackageDescription/Product.swift +++ b/Sources/PackageDescription/Product.swift @@ -71,9 +71,14 @@ public class Product { public final class Executable: Product, @unchecked Sendable { /// The names of the targets in this product. public let targets: [String] + + /// Any specific product settings that apply to this product. + @_spi(PackageProductSettings) + public let settings: [ProductSetting] - init(name: String, targets: [String]) { + init(name: String, targets: [String], settings: [ProductSetting]) { self.targets = targets + self.settings = settings super.init(name: name) } } @@ -152,7 +157,16 @@ public class Product { name: String, targets: [String] ) -> Product { - return Executable(name: name, targets: targets) + return Executable(name: name, targets: targets, settings: []) + } + + @_spi(PackageProductSettings) + public static func executable( + name: String, + targets: [String], + settings: [ProductSetting] + ) -> Product { + return Executable(name: name, targets: targets, settings: settings) } /// Defines a product that vends a package plugin target for use by clients of the package. @@ -174,3 +188,374 @@ public class Product { return Plugin(name: name, targets: targets) } } + + +/// A particular setting to apply to a product. Some may be specific to certain platforms. +#if ENABLE_APPLE_PRODUCT_TYPES +public enum ProductSetting: Equatable { + case bundleIdentifier(String) + case teamIdentifier(String) + case displayVersion(String) + case bundleVersion(String) + case iOSAppInfo(IOSAppInfo) + + public struct IOSAppInfo: Equatable { + var appIcon: AppIcon? + var accentColor: AccentColor? + var supportedDeviceFamilies: [DeviceFamily] + var supportedInterfaceOrientations: [InterfaceOrientation] + var capabilities: [Capability] = [] + var appCategory: AppCategory? + var additionalInfoPlistContentFilePath: String? + + // Represents the configuration of the app's accent color. + public enum AccentColor: Equatable { + public struct PresetColor: Equatable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + } + // Predefined color. + case presetColor(PresetColor) + // Named asset in an asset catalog. + case asset(String) + } + + // Represents the configuration of the app's app icon. + public enum AppIcon: Equatable { + public struct PlaceholderIcon: Equatable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + } + // Placeholder app icon using the app's accent color and specified icon. + case placeholder(icon: PlaceholderIcon) + // Named asset in an asset catalog. + case asset(String) + } + + /// Represents a family of device types that an application can support. + public enum DeviceFamily: String, Equatable { + case phone + case pad + case mac + } + + /// Represents a condition on a particular device family. + public struct DeviceFamilyCondition: Equatable { + public var deviceFamilies: [DeviceFamily] + + public init(deviceFamilies: [DeviceFamily]) { + self.deviceFamilies = deviceFamilies + } + public static func when(deviceFamilies: [DeviceFamily]) -> DeviceFamilyCondition { + return DeviceFamilyCondition(deviceFamilies: deviceFamilies) + } + } + + /// Represents a supported device interface orientation. + public enum InterfaceOrientation: Equatable { + case portrait(_ condition: DeviceFamilyCondition? = nil) + case portraitUpsideDown(_ condition: DeviceFamilyCondition? = nil) + case landscapeRight(_ condition: DeviceFamilyCondition? = nil) + case landscapeLeft(_ condition: DeviceFamilyCondition? = nil) + + public static var portrait: Self { portrait(nil) } + public static var portraitUpsideDown: Self { portraitUpsideDown(nil) } + public static var landscapeRight: Self { landscapeRight(nil) } + public static var landscapeLeft: Self { landscapeLeft(nil) } + } + + /// A capability required by the device. + public enum Capability: Equatable { + case appTransportSecurity(configuration: AppTransportSecurityConfiguration, _ condition: DeviceFamilyCondition? = nil) + case bluetoothAlways(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case calendars(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case camera(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case contacts(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case faceID(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case fileAccess(_ location: FileAccessLocation, mode: FileAccessMode, _ condition: DeviceFamilyCondition? = nil) + case incomingNetworkConnections(_ condition: DeviceFamilyCondition? = nil) + case localNetwork(purposeString: String, bonjourServiceTypes: [String]? = nil, _ condition: DeviceFamilyCondition? = nil) + case locationAlwaysAndWhenInUse(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case locationWhenInUse(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case mediaLibrary(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case microphone(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case motion(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case nearbyInteractionAllowOnce(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case outgoingNetworkConnections(_ condition: DeviceFamilyCondition? = nil) + case photoLibrary(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case photoLibraryAdd(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case reminders(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case speechRecognition(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + case userTracking(purposeString: String, _ condition: DeviceFamilyCondition? = nil) + } + + public struct AppTransportSecurityConfiguration: Equatable { + public var allowsArbitraryLoadsInWebContent: Bool? = nil + public var allowsArbitraryLoadsForMedia: Bool? = nil + public var allowsLocalNetworking: Bool? = nil + public var exceptionDomains: [ExceptionDomain]? = nil + public var pinnedDomains: [PinnedDomain]? = nil + + public struct ExceptionDomain: Equatable { + public var domainName: String + public var includesSubdomains: Bool? = nil + public var exceptionAllowsInsecureHTTPLoads: Bool? = nil + public var exceptionMinimumTLSVersion: String? = nil + public var exceptionRequiresForwardSecrecy: Bool? = nil + public var requiresCertificateTransparency: Bool? = nil + + public init( + domainName: String, + includesSubdomains: Bool? = nil, + exceptionAllowsInsecureHTTPLoads: Bool? = nil, + exceptionMinimumTLSVersion: String? = nil, + exceptionRequiresForwardSecrecy: Bool? = nil, + requiresCertificateTransparency: Bool? = nil + ) { + self.domainName = domainName + self.includesSubdomains = includesSubdomains + self.exceptionAllowsInsecureHTTPLoads = exceptionAllowsInsecureHTTPLoads + self.exceptionMinimumTLSVersion = exceptionMinimumTLSVersion + self.exceptionRequiresForwardSecrecy = exceptionRequiresForwardSecrecy + self.requiresCertificateTransparency = requiresCertificateTransparency + } + } + + public struct PinnedDomain: Equatable { + public var domainName: String + public var includesSubdomains : Bool? = nil + public var pinnedCAIdentities : [[String: String]]? = nil + public var pinnedLeafIdentities : [[String: String]]? = nil + + public init( + domainName: String, + includesSubdomains: Bool? = nil, + pinnedCAIdentities: [[String: String]]? = nil, + pinnedLeafIdentities: [[String: String]]? = nil + ) { + self.domainName = domainName + self.includesSubdomains = includesSubdomains + self.pinnedCAIdentities = pinnedCAIdentities + self.pinnedLeafIdentities = pinnedLeafIdentities + } + } + + public init( + allowsArbitraryLoadsInWebContent: Bool? = nil, + allowsArbitraryLoadsForMedia: Bool? = nil, + allowsLocalNetworking: Bool? = nil, + exceptionDomains: [ExceptionDomain]? = nil, + pinnedDomains: [PinnedDomain]? = nil + ) { + self.allowsArbitraryLoadsInWebContent = allowsArbitraryLoadsInWebContent + self.allowsArbitraryLoadsForMedia = allowsArbitraryLoadsForMedia + self.allowsLocalNetworking = allowsLocalNetworking + self.exceptionDomains = exceptionDomains + self.pinnedDomains = pinnedDomains + } + } + + public enum FileAccessLocation: Equatable { + case userSelectedFiles + case downloadsFolder + case pictureFolder + case musicFolder + case moviesFolder + + var identifier: String { + switch self { + case .userSelectedFiles: + return "userSelectedFiles" + case .downloadsFolder: + return "downloadsFolder" + case .pictureFolder: + return "pictureFolder" + case .musicFolder: + return "musicFolder" + case .moviesFolder: + return "moviesFolder" + } + } + } + + public enum FileAccessMode: Equatable { + case readOnly + case readWrite + + var identifier: String { + switch self { + case .readOnly: return "readOnly" + case .readWrite: return "readWrite" + } + } + } + + public struct AppCategory: Equatable, ExpressibleByStringLiteral { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public init(stringLiteral value: StringLiteralType) { + self.init(rawValue: value) + } + } + + public init( + appIcon: AppIcon?, + accentColor: AccentColor?, + supportedDeviceFamilies: [DeviceFamily], + supportedInterfaceOrientations: [InterfaceOrientation], + capabilities: [Capability], + appCategory: AppCategory?, + additionalInfoPlistContentFilePath: String? + ) { + self.appIcon = appIcon + self.accentColor = accentColor + self.supportedDeviceFamilies = supportedDeviceFamilies + self.supportedInterfaceOrientations = supportedInterfaceOrientations + self.capabilities = capabilities + self.appCategory = appCategory + self.additionalInfoPlistContentFilePath = additionalInfoPlistContentFilePath + } + } +} +#else +// This has to be defined at least as SPI because some of the methods that are +// SPI use it, but it doesn't contain anything when Apple product types aren't +// enabled. +@_spi(PackageProductSettings) +public enum ProductSetting: Equatable { } +#endif + +#if ENABLE_APPLE_PRODUCT_TYPES +extension ProductSetting.IOSAppInfo.AccentColor.PresetColor { + public static var blue: Self { .init(rawValue: "blue") } + public static var brown: Self { .init(rawValue: "brown") } + public static var cyan: Self { .init(rawValue: "cyan") } + public static var green: Self { .init(rawValue: "green") } + public static var indigo: Self { .init(rawValue: "indigo") } + public static var mint: Self { .init(rawValue: "mint") } + public static var orange: Self { .init(rawValue: "orange") } + public static var pink: Self { .init(rawValue: "pink") } + public static var purple: Self { .init(rawValue: "purple") } + public static var red: Self { .init(rawValue: "red") } + public static var teal: Self { .init(rawValue: "teal") } + public static var yellow: Self { .init(rawValue: "yellow") } +} + +extension ProductSetting.IOSAppInfo.AppIcon.PlaceholderIcon { + public static var bandage: Self { .init(rawValue: "bandage")} + public static var barChart: Self { .init(rawValue: "barChart")} + public static var beachball: Self { .init(rawValue: "beachball")} + public static var bicycle: Self { .init(rawValue: "bicycle")} + public static var binoculars: Self { .init(rawValue: "binoculars")} + public static var bird: Self { .init(rawValue: "bird") } + public static var boat: Self { .init(rawValue: "boat")} + public static var bowl: Self { .init(rawValue: "bowl")} + public static var box: Self { .init(rawValue: "box")} + public static var bunny: Self { .init(rawValue: "bunny")} + public static var butterfly: Self { .init(rawValue: "butterfly")} + public static var calculator: Self { .init(rawValue: "calculator")} + public static var calendar: Self { .init(rawValue: "calendar")} + public static var camera: Self { .init(rawValue: "camera")} + public static var car: Self { .init(rawValue: "car")} + public static var carrot: Self { .init(rawValue: "carrot")} + public static var cat: Self { .init(rawValue: "cat")} + public static var chatMessage: Self { .init(rawValue: "chatMessage")} + public static var checkmark: Self { .init(rawValue: "checkmark")} + public static var clock: Self { .init(rawValue: "clock")} + public static var cloud: Self { .init(rawValue: "cloud")} + public static var coffee: Self { .init(rawValue: "coffee")} + public static var coins: Self { .init(rawValue: "coins")} + public static var dog: Self { .init(rawValue: "dog")} + public static var earth: Self { .init(rawValue: "earth")} + public static var flower: Self { .init(rawValue: "flower")} + public static var gamepad: Self { .init(rawValue: "gamepad")} + public static var gift: Self { .init(rawValue: "gift")} + public static var heart: Self { .init(rawValue: "heart")} + public static var images: Self { .init(rawValue: "images")} + public static var leaf: Self { .init(rawValue: "leaf")} + public static var lightningBolt: Self { .init(rawValue: "lightningBolt")} + public static var location: Self { .init(rawValue: "location")} + public static var magicWand: Self { .init(rawValue: "magicWand")} + public static var map: Self { .init(rawValue: "map")} + public static var mic: Self { .init(rawValue: "mic")} + public static var moon: Self { .init(rawValue: "moon")} + public static var movieReel: Self { .init(rawValue: "movieReel")} + public static var note: Self { .init(rawValue: "note")} + public static var openBook: Self { .init(rawValue: "openBook")} + public static var palette: Self { .init(rawValue: "palette")} + public static var paper: Self { .init(rawValue: "paper")} + public static var pencil: Self { .init(rawValue: "pencil")} + public static var plane: Self { .init(rawValue: "plane")} + public static var rocket: Self { .init(rawValue: "rocket")} + public static var running: Self { .init(rawValue: "running")} + public static var sandwich: Self { .init(rawValue: "sandwich")} + public static var smiley: Self { .init(rawValue: "smiley")} + public static var sparkle: Self { .init(rawValue: "sparkle")} + public static var star: Self { .init(rawValue: "star")} + public static var sun: Self { .init(rawValue: "sun")} + public static var tv: Self { .init(rawValue: "tv")} + public static var twoPeople: Self { .init(rawValue: "twoPeople")} + public static var weights: Self { .init(rawValue: "weights")} +} + +extension ProductSetting.IOSAppInfo.AppCategory { + public static var books: Self { .init(rawValue: "public.app-category.books") } + public static var business: Self { .init(rawValue: "public.app-category.business") } + public static var developerTools: Self { .init(rawValue: "public.app-category.developer-tools") } + public static var education: Self { .init(rawValue: "public.app-category.education") } + public static var entertainment: Self { .init(rawValue: "public.app-category.entertainment") } + public static var finance: Self { .init(rawValue: "public.app-category.finance") } + public static var foodAndDrink: Self { .init(rawValue: "public.app-category.food-and-drink") } + public static var graphicsDesign: Self { .init(rawValue: "public.app-category.graphics-design") } + public static var healthcareFitness: Self { .init(rawValue: "public.app-category.healthcare-fitness") } + public static var lifestyle: Self { .init(rawValue: "public.app-category.lifestyle") } + public static var magazinesAndNewspapers: Self { .init(rawValue: "public.app-category.magazines-and-newspapers") } + public static var medical: Self { .init(rawValue: "public.app-category.medical") } + public static var music: Self { .init(rawValue: "public.app-category.music") } + public static var navigation: Self { .init(rawValue: "public.app-category.navigation") } + public static var news: Self { .init(rawValue: "public.app-category.news") } + public static var photography: Self { .init(rawValue: "public.app-category.photography") } + public static var productivity: Self { .init(rawValue: "public.app-category.productivity") } + public static var reference: Self { .init(rawValue: "public.app-category.reference") } + public static var shopping: Self { .init(rawValue: "public.app-category.shopping") } + public static var socialNetworking: Self { .init(rawValue: "public.app-category.social-networking") } + public static var sports: Self { .init(rawValue: "public.app-category.sports") } + public static var travel: Self { .init(rawValue: "public.app-category.travel") } + public static var utilities: Self { .init(rawValue: "public.app-category.utilities") } + public static var video: Self { .init(rawValue: "public.app-category.video") } + public static var weather: Self { .init(rawValue: "public.app-category.weather") } + + // Games + public static var games: Self { .init(rawValue: "public.app-category.games") } + // Games subcategories + public static var actionGames: Self { .init(rawValue: "public.app-category.action-games") } + public static var adventureGames: Self { .init(rawValue: "public.app-category.adventure-games") } + public static var arcadeGames: Self { .init(rawValue: "public.app-category.arcade-games") } + public static var boardGames: Self { .init(rawValue: "public.app-category.board-games") } + public static var cardGames: Self { .init(rawValue: "public.app-category.card-games") } + public static var casinoGames: Self { .init(rawValue: "public.app-category.casino-games") } + public static var diceGames: Self { .init(rawValue: "public.app-category.dice-games") } + public static var educationalGames: Self { .init(rawValue: "public.app-category.educational-games") } + public static var familyGames: Self { .init(rawValue: "public.app-category.family-games") } + public static var kidsGames: Self { .init(rawValue: "public.app-category.kids-games") } + public static var musicGames: Self { .init(rawValue: "public.app-category.music-games") } + public static var puzzleGames: Self { .init(rawValue: "public.app-category.puzzle-games") } + public static var racingGames: Self { .init(rawValue: "public.app-category.racing-games") } + public static var rolePlayingGames: Self { .init(rawValue: "public.app-category.role-playing-games") } + public static var simulationGames: Self { .init(rawValue: "public.app-category.simulation-games") } + public static var sportsGames: Self { .init(rawValue: "public.app-category.sports-games") } + public static var strategyGames: Self { .init(rawValue: "public.app-category.strategy-games") } + public static var triviaGames: Self { .init(rawValue: "public.app-category.trivia-games") } + public static var wordGames: Self { .init(rawValue: "public.app-category.word-games") } +} +#endif diff --git a/Sources/PackageLoading/ManifestJSONParser.swift b/Sources/PackageLoading/ManifestJSONParser.swift index 99ed2a98fdb..37fccbf4bff 100644 --- a/Sources/PackageLoading/ManifestJSONParser.swift +++ b/Sources/PackageLoading/ManifestJSONParser.swift @@ -302,6 +302,195 @@ extension PackageDependency.Registry.Requirement { } } +#if ENABLE_APPLE_PRODUCT_TYPES +extension ProductSetting { + init(_ setting: Serialization.ProductSetting) { + switch setting { + case .bundleIdentifier(let value): + self = .bundleIdentifier(value) + case .teamIdentifier(let value): + self = .teamIdentifier(value) + case .displayVersion(let value): + self = .displayVersion(value) + case .bundleVersion(let value): + self = .bundleVersion(value) + case .iOSAppInfo(let appInfo): + self = .iOSAppInfo(.init(appInfo)) + } + } +} + +extension ProductSetting.IOSAppInfo { + init(_ appInfo: Serialization.ProductSetting.IOSAppInfo) { + self.init( + appIcon: appInfo.appIcon.map { .init($0) }, + accentColor: appInfo.accentColor.map { .init($0) }, + supportedDeviceFamilies: appInfo.supportedDeviceFamilies.map { .init($0) }, + supportedInterfaceOrientations: appInfo.supportedInterfaceOrientations.map { .init($0) }, + capabilities: appInfo.capabilities.map { .init($0) }, + appCategory: appInfo.appCategory.map { .init($0) }, + additionalInfoPlistContentFilePath: appInfo.additionalInfoPlistContentFilePath + ) + } +} + +extension ProductSetting.IOSAppInfo.DeviceFamily { + init(_ deviceFamily: Serialization.ProductSetting.IOSAppInfo.DeviceFamily) { + switch deviceFamily { + case .phone: self = .phone + case .pad: self = .pad + case .mac: self = .mac + } + } +} + +extension ProductSetting.IOSAppInfo.DeviceFamilyCondition { + init(_ condition: Serialization.ProductSetting.IOSAppInfo.DeviceFamilyCondition) { + self.init(deviceFamilies: condition.deviceFamilies.map { .init($0) }) + } +} + +extension ProductSetting.IOSAppInfo.InterfaceOrientation { + init(_ interfaceOrientation: Serialization.ProductSetting.IOSAppInfo.InterfaceOrientation) { + switch interfaceOrientation { + case .portrait(let condition): + self = .portrait(condition: condition.map { .init($0) }) + case .portraitUpsideDown(let condition): + self = .portraitUpsideDown(condition: condition.map { .init($0) }) + case .landscapeRight(let condition): + self = .landscapeRight(condition: condition.map { .init($0) }) + case .landscapeLeft(let condition): + self = .landscapeLeft(condition: condition.map { .init($0) }) + } + } +} + +extension ProductSetting.IOSAppInfo.AppIcon { + init(_ icon: Serialization.ProductSetting.IOSAppInfo.AppIcon) { + switch icon { + case .placeholder(icon: let icon): + self = .placeholder(icon: .init(icon)) + case .asset(let name): + self = .asset(name: name) + } + } +} + +extension ProductSetting.IOSAppInfo.AppIcon.PlaceholderIcon { + init(_ icon: Serialization.ProductSetting.IOSAppInfo.AppIcon.PlaceholderIcon) { + self.init(rawValue: icon.rawValue) + } +} + +extension ProductSetting.IOSAppInfo.AccentColor { + init(_ color: Serialization.ProductSetting.IOSAppInfo.AccentColor) { + switch color { + case .presetColor(let color): + self = .presetColor(presetColor: .init(color)) + case .asset(let name): + self = .asset(name: name) + } + } +} + +extension ProductSetting.IOSAppInfo.AccentColor.PresetColor { + init(_ color: Serialization.ProductSetting.IOSAppInfo.AccentColor.PresetColor) { + self.init(rawValue: color.rawValue) + } +} + +extension ProductSetting.IOSAppInfo.Capability { + init(_ capability: Serialization.ProductSetting.IOSAppInfo.Capability) { + switch capability { + case .appTransportSecurity(configuration: let configuration, let condition): + self.init(purpose: "appTransportSecurity", appTransportSecurityConfiguration: .init(configuration), condition: condition.map { .init($0) }) + case .bluetoothAlways(purposeString: let purposeString, let condition): + self.init(purpose: "bluetoothAlways", purposeString: purposeString, condition: condition.map { .init($0) }) + case .calendars(purposeString: let purposeString, let condition): + self.init(purpose: "calendars", purposeString: purposeString, condition: condition.map { .init($0) }) + case .camera(purposeString: let purposeString, let condition): + self.init(purpose: "camera", purposeString: purposeString, condition: condition.map { .init($0) }) + case .contacts(purposeString: let purposeString, let condition): + self.init(purpose: "contacts", purposeString: purposeString, condition: condition.map { .init($0) }) + case .faceID(purposeString: let purposeString, let condition): + self.init(purpose: "faceID", purposeString: purposeString, condition: condition.map { .init($0) }) + case .fileAccess(let location, mode: let mode, let condition): + self.init(purpose: "fileAccess", fileAccessLocation: location.rawValue, fileAccessMode: mode.rawValue, condition: condition.map { .init($0) }) + case .incomingNetworkConnections(let condition): + self.init(purpose: "incomingNetworkConnections", condition: condition.map { .init($0) }) + case .localNetwork(purposeString: let purposeString, bonjourServiceTypes: let bonjourServiceTypes, let condition): + self.init(purpose: "localNetwork", purposeString: purposeString, bonjourServiceTypes: bonjourServiceTypes, condition: condition.map { .init($0) }) + case .locationAlwaysAndWhenInUse(purposeString: let purposeString, let condition): + self.init(purpose: "locationAlwaysAndWhenInUse", purposeString: purposeString, condition: condition.map { .init($0) }) + case .locationWhenInUse(purposeString: let purposeString, let condition): + self.init(purpose: "locationWhenInUse", purposeString: purposeString, condition: condition.map { .init($0) }) + case .mediaLibrary(purposeString: let purposeString, let condition): + self.init(purpose: "mediaLibrary", purposeString: purposeString, condition: condition.map { .init($0) }) + case .microphone(purposeString: let purposeString, let condition): + self.init(purpose: "microphone", purposeString: purposeString, condition: condition.map { .init($0) }) + case .motion(purposeString: let purposeString, let condition): + self.init(purpose: "motion", purposeString: purposeString, condition: condition.map { .init($0) }) + case .nearbyInteractionAllowOnce(purposeString: let purposeString, let condition): + self.init(purpose: "nearbyInteractionAllowOnce", purposeString: purposeString, condition: condition.map { .init($0) }) + case .outgoingNetworkConnections(let condition): + self.init(purpose: "outgoingNetworkConnections", condition: condition.map { .init($0) }) + case .photoLibrary(purposeString: let purposeString, let condition): + self.init(purpose: "photoLibrary", purposeString: purposeString, condition: condition.map { .init($0) }) + case .photoLibraryAdd(purposeString: let purposeString, let condition): + self.init(purpose: "photoLibraryAdd", purposeString: purposeString, condition: condition.map { .init($0) }) + case .reminders(purposeString: let purposeString, let condition): + self.init(purpose: "reminders", purposeString: purposeString, condition: condition.map { .init($0) }) + case .speechRecognition(purposeString: let purposeString, let condition): + self.init(purpose: "speechRecognition", purposeString: purposeString, condition: condition.map { .init($0) }) + case .userTracking(purposeString: let purposeString, let condition): + self.init(purpose: "userTracking", purposeString: purposeString, condition: condition.map { .init($0) }) + } + } +} + +extension ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration { + init(_ configuration: Serialization.ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration) { + self.init( + allowsArbitraryLoadsInWebContent: configuration.allowsArbitraryLoadsInWebContent, + allowsArbitraryLoadsForMedia: configuration.allowsArbitraryLoadsForMedia, + allowsLocalNetworking: configuration.allowsLocalNetworking, + exceptionDomains: configuration.exceptionDomains?.map { .init($0) }, + pinnedDomains: configuration.pinnedDomains?.map { .init($0) } + ) + } +} + +extension ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.ExceptionDomain { + init(_ exceptionDomain: Serialization.ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.ExceptionDomain) { + self.init( + domainName: exceptionDomain.domainName, + includesSubdomains: exceptionDomain.includesSubdomains, + exceptionAllowsInsecureHTTPLoads: exceptionDomain.exceptionAllowsInsecureHTTPLoads, + exceptionMinimumTLSVersion: exceptionDomain.exceptionMinimumTLSVersion, + exceptionRequiresForwardSecrecy: exceptionDomain.exceptionRequiresForwardSecrecy, + requiresCertificateTransparency: exceptionDomain.requiresCertificateTransparency + ) + } +} + +extension ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.PinnedDomain { + init(_ pinnedDomain: Serialization.ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.PinnedDomain) { + self.init( + domainName: pinnedDomain.domainName, + includesSubdomains: pinnedDomain.includesSubdomains, + pinnedCAIdentities: pinnedDomain.pinnedCAIdentities, + pinnedLeafIdentities: pinnedDomain.pinnedLeafIdentities + ) + } +} + +extension ProductSetting.IOSAppInfo.AppCategory { + init(_ category: Serialization.ProductSetting.IOSAppInfo.AppCategory) { + self.init(rawValue: category.rawValue) + } +} +#endif + extension ProductDescription { init(_ product: Serialization.Product) throws { let productType: ProductType @@ -313,7 +502,11 @@ extension ProductDescription { case .library(let type): productType = .library(.init(type)) } + #if ENABLE_APPLE_PRODUCT_TYPES + try self.init(name: product.name, type: productType, targets: product.targets, settings: product.settings.map { .init($0) }) + #else try self.init(name: product.name, type: productType, targets: product.targets) + #endif } } diff --git a/Sources/PackageLoading/ManifestLoader.swift b/Sources/PackageLoading/ManifestLoader.swift index 07ba9ebb482..09482d86952 100644 --- a/Sources/PackageLoading/ManifestLoader.swift +++ b/Sources/PackageLoading/ManifestLoader.swift @@ -879,9 +879,15 @@ public final class ManifestLoader: ManifestLoaderProtocol { if runtimePath.extension == "framework" { cmd += [ "-F", runtimePath.parentDirectory.pathString, - "-framework", "PackageDescription", "-Xlinker", "-rpath", "-Xlinker", runtimePath.parentDirectory.pathString, ] + + // Explicitly link `AppleProductTypes` since auto-linking won't work here. +#if ENABLE_APPLE_PRODUCT_TYPES + cmd += ["-framework", "AppleProductTypes"] +#else + cmd += ["-framework", "PackageDescription"] +#endif } else { cmd += [ "-L", runtimePath.pathString, diff --git a/Sources/PackageModel/Manifest/ProductDescription.swift b/Sources/PackageModel/Manifest/ProductDescription.swift index dc0f05349f2..3d761bfc5ed 100644 --- a/Sources/PackageModel/Manifest/ProductDescription.swift +++ b/Sources/PackageModel/Manifest/ProductDescription.swift @@ -24,10 +24,14 @@ public struct ProductDescription: Hashable, Codable, Sendable { /// The type of product. public let type: ProductType + /// The product-specific settings declared for this product. + public let settings: [ProductSetting] + public init( name: String, type: ProductType, - targets: [String] + targets: [String], + settings: [ProductSetting] = [] ) throws { guard type != .test else { throw InternalError("Declaring test products isn't supported: \(name):\(targets)") @@ -35,5 +39,252 @@ public struct ProductDescription: Hashable, Codable, Sendable { self.name = name self.type = type self.targets = targets + self.settings = settings + } +} + +/// A particular setting to apply to a product. Some may be specific to certain platforms. +public enum ProductSetting: Equatable, Codable, Sendable, Hashable { + case bundleIdentifier(String) + case teamIdentifier(String) + case displayVersion(String) + case bundleVersion(String) + case iOSAppInfo(IOSAppInfo) + + public struct IOSAppInfo: Equatable, Codable, Sendable, Hashable { + public var appIcon: AppIcon? + public var accentColor: AccentColor? + public var supportedDeviceFamilies: [DeviceFamily] + public var supportedInterfaceOrientations: [InterfaceOrientation] + public var capabilities: [Capability] + public var appCategory: AppCategory? + public var additionalInfoPlistContentFilePath: String? + + public enum DeviceFamily: String, Equatable, Codable, Sendable, Hashable { + case pad + case phone + case mac + } + + public struct DeviceFamilyCondition: Equatable, Codable, Sendable, Hashable { + public let deviceFamilies: [DeviceFamily] + public init(deviceFamilies: [DeviceFamily]) { + self.deviceFamilies = deviceFamilies + } + } + + public enum InterfaceOrientation: Equatable, Codable, Sendable, Hashable { + case portrait(condition: DeviceFamilyCondition?) + case portraitUpsideDown(condition: DeviceFamilyCondition?) + case landscapeRight(condition: DeviceFamilyCondition?) + case landscapeLeft(condition: DeviceFamilyCondition?) + } + + public enum AppIcon: Equatable, Codable, Sendable, Hashable { + public struct PlaceholderIcon: Equatable, Codable, Sendable, Hashable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + case placeholder(icon: PlaceholderIcon) + case asset(name: String) + } + + public enum AccentColor: Equatable, Codable, Sendable, Hashable { + public struct PresetColor: Equatable, Codable, Sendable, Hashable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + case presetColor(presetColor: PresetColor) + case asset(name: String) + } + + public struct Capability: Equatable, Codable, Sendable, Hashable { + public var purpose: String + public var purposeString: String? + public var appTransportSecurityConfiguration: AppTransportSecurityConfiguration? + public var bonjourServiceTypes: [String]? + public var fileAccessLocation: String? + public var fileAccessMode: String? + public var condition: DeviceFamilyCondition? + + public init( + purpose: String, + purposeString: String? = nil, + appTransportSecurityConfiguration: AppTransportSecurityConfiguration? = nil, + bonjourServiceTypes: [String]? = nil, + fileAccessLocation: String? = nil, + fileAccessMode: String? = nil, + condition: DeviceFamilyCondition? + ) { + self.purpose = purpose + self.purposeString = purposeString + self.appTransportSecurityConfiguration = appTransportSecurityConfiguration + self.bonjourServiceTypes = bonjourServiceTypes + self.fileAccessLocation = fileAccessLocation + self.fileAccessMode = fileAccessMode + self.condition = condition + } + } + + public struct AppTransportSecurityConfiguration: Equatable, Codable, Sendable, Hashable { + public var allowsArbitraryLoadsInWebContent: Bool? = nil + public var allowsArbitraryLoadsForMedia: Bool? = nil + public var allowsLocalNetworking: Bool? = nil + public var exceptionDomains: [ExceptionDomain]? = nil + public var pinnedDomains: [PinnedDomain]? = nil + + public struct ExceptionDomain: Equatable, Codable, Sendable, Hashable { + public var domainName: String + public var includesSubdomains: Bool? = nil + public var exceptionAllowsInsecureHTTPLoads: Bool? = nil + public var exceptionMinimumTLSVersion: String? = nil + public var exceptionRequiresForwardSecrecy: Bool? = nil + public var requiresCertificateTransparency: Bool? = nil + + public init( + domainName: String, + includesSubdomains: Bool?, + exceptionAllowsInsecureHTTPLoads: Bool?, + exceptionMinimumTLSVersion: String?, + exceptionRequiresForwardSecrecy: Bool?, + requiresCertificateTransparency: Bool? + ) { + self.domainName = domainName + self.includesSubdomains = includesSubdomains + self.exceptionAllowsInsecureHTTPLoads = exceptionAllowsInsecureHTTPLoads + self.exceptionMinimumTLSVersion = exceptionMinimumTLSVersion + self.exceptionRequiresForwardSecrecy = exceptionRequiresForwardSecrecy + self.requiresCertificateTransparency = requiresCertificateTransparency + } + } + + public struct PinnedDomain: Equatable, Codable, Sendable, Hashable { + public var domainName: String + public var includesSubdomains : Bool? = nil + public var pinnedCAIdentities : [[String: String]]? = nil + public var pinnedLeafIdentities : [[String: String]]? = nil + + public init( + domainName: String, + includesSubdomains: Bool?, + pinnedCAIdentities : [[String: String]]? , + pinnedLeafIdentities : [[String: String]]? + ) { + self.domainName = domainName + self.includesSubdomains = includesSubdomains + self.pinnedCAIdentities = pinnedCAIdentities + self.pinnedLeafIdentities = pinnedLeafIdentities + } + } + + public init( + allowsArbitraryLoadsInWebContent: Bool?, + allowsArbitraryLoadsForMedia: Bool?, + allowsLocalNetworking: Bool?, + exceptionDomains: [ExceptionDomain]?, + pinnedDomains: [PinnedDomain]? + ) { + self.allowsArbitraryLoadsInWebContent = allowsArbitraryLoadsInWebContent + self.allowsArbitraryLoadsForMedia = allowsArbitraryLoadsForMedia + self.allowsLocalNetworking = allowsLocalNetworking + self.exceptionDomains = exceptionDomains + self.pinnedDomains = pinnedDomains + } + } + + public struct AppCategory: Equatable, Codable, Sendable, Hashable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + public init( + appIcon: AppIcon?, + accentColor: AccentColor?, + supportedDeviceFamilies: [DeviceFamily], + supportedInterfaceOrientations: [InterfaceOrientation], + capabilities: [Capability], + appCategory: AppCategory?, + additionalInfoPlistContentFilePath: String? + ) { + self.appIcon = appIcon + self.accentColor = accentColor + self.supportedDeviceFamilies = supportedDeviceFamilies + self.supportedInterfaceOrientations = supportedInterfaceOrientations + self.capabilities = capabilities + self.appCategory = appCategory + self.additionalInfoPlistContentFilePath = additionalInfoPlistContentFilePath + } + } +} + + +extension ProductSetting { + private enum CodingKeys: String, CodingKey { + case bundleIdentifier + case teamIdentifier + case displayVersion + case bundleVersion + case iOSAppInfo + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .bundleIdentifier(value): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .bundleIdentifier) + try unkeyedContainer.encode(value) + case let .teamIdentifier(value): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .teamIdentifier) + try unkeyedContainer.encode(value) + case let .displayVersion(value): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .displayVersion) + try unkeyedContainer.encode(value) + case let .bundleVersion(value): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .bundleVersion) + try unkeyedContainer.encode(value) + case let .iOSAppInfo(value): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .iOSAppInfo) + try unkeyedContainer.encode(value) + } + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + guard let key = values.allKeys.first(where: values.contains) else { + throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Did not find a matching key")) + } + switch key { + case .bundleIdentifier: + var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key) + let value = try unkeyedValues.decode(String.self) + self = .bundleIdentifier(value) + case .teamIdentifier: + var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key) + let value = try unkeyedValues.decode(String.self) + self = .teamIdentifier(value) + case .displayVersion: + var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key) + let value = try unkeyedValues.decode(String.self) + self = .displayVersion(value) + case .bundleVersion: + var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key) + let value = try unkeyedValues.decode(String.self) + self = .bundleVersion(value) + case .iOSAppInfo: + var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key) + let value = try unkeyedValues.decode(IOSAppInfo.self) + self = .iOSAppInfo(value) + } } } diff --git a/Sources/PackageModel/ManifestSourceGeneration.swift b/Sources/PackageModel/ManifestSourceGeneration.swift index 5994b145246..e01f5c60979 100644 --- a/Sources/PackageModel/ManifestSourceGeneration.swift +++ b/Sources/PackageModel/ManifestSourceGeneration.swift @@ -34,14 +34,18 @@ extension Manifest { packageDirectory: AbsolutePath, toolsVersionHeaderComment: String? = .none, additionalImportModuleNames: [String] = [], - customProductTypeSourceGenerator: ManifestCustomProductTypeSourceGenerator? = .none + customProductTypeSourceGenerator: ManifestCustomProductTypeSourceGenerator? = .none, + overridingToolsVersion: ToolsVersion? = nil ) rethrows -> String { + let toolsVersion = overridingToolsVersion ?? self.toolsVersion + // Generate the source code fragment for the top level of the package // expression. let packageExprFragment = try SourceCodeFragment( from: self, packageDirectory: packageDirectory, - customProductTypeSourceGenerator: customProductTypeSourceGenerator) + customProductTypeSourceGenerator: customProductTypeSourceGenerator, + toolsVersion: toolsVersion) // Generate the source code from the module names and code fragment. // We only write out the major and minor (not patch) versions of the @@ -68,7 +72,8 @@ fileprivate extension SourceCodeFragment { init( from manifest: Manifest, packageDirectory: AbsolutePath, - customProductTypeSourceGenerator: ManifestCustomProductTypeSourceGenerator? + customProductTypeSourceGenerator: ManifestCustomProductTypeSourceGenerator?, + toolsVersion: ToolsVersion ) rethrows { var params: [SourceCodeFragment] = [] @@ -93,7 +98,7 @@ fileprivate extension SourceCodeFragment { } if !manifest.products.isEmpty { - let nodes = try manifest.products.map{ try SourceCodeFragment(from: $0, customProductTypeSourceGenerator: customProductTypeSourceGenerator) } + let nodes = try manifest.products.map{ try SourceCodeFragment(from: $0, customProductTypeSourceGenerator: customProductTypeSourceGenerator, toolsVersion: toolsVersion) } params.append(SourceCodeFragment(key: "products", subnodes: nodes)) } @@ -192,7 +197,7 @@ fileprivate extension SourceCodeFragment { /// Instantiates a SourceCodeFragment to represent a single product. If there's a custom product generator, it gets /// a chance to generate the source code fragments before checking the default types. - init(from product: ProductDescription, customProductTypeSourceGenerator: ManifestCustomProductTypeSourceGenerator?) rethrows { + init(from product: ProductDescription, customProductTypeSourceGenerator: ManifestCustomProductTypeSourceGenerator?, toolsVersion: ToolsVersion) rethrows { // Use a custom source code fragment if we have a custom generator and it returns a value. if let customSubnode = try customProductTypeSourceGenerator?(product) { self = customSubnode @@ -214,7 +219,36 @@ fileprivate extension SourceCodeFragment { } self.init(enum: "library", subnodes: params, multiline: true) case .executable: - self.init(enum: "executable", subnodes: params, multiline: true) + // For iOSApplication targets, we temporarily do something special + // This will be generalized once we are sure of how it should look. + let isIOSApp = product.settings.contains(where: { + // iOS apps are currently identifier by an iOSAppInfo product + // setting. + if case .iOSAppInfo(_) = $0 { + return true + } + return false + }) + if isIOSApp { + // Create a parameter for each of the product settings. + for setting in product.settings { + let subnode = SourceCodeFragment(from: setting, toolsVersion: toolsVersion) + switch setting { + case .iOSAppInfo(_): + // For the app info only, we hoist the subnodes of the + // initializer out to the top level, since that is the + // form of the instantiator function. + params.append(contentsOf: subnode.subnodes?.first?.subnodes ?? []) + default: + // Other product settings are just added as they are. + params.append(subnode) + } + } + self.init(enum: "iOSApplication", subnodes: params, multiline: true) + } + else { + self.init(enum: "executable", subnodes: params, multiline: true) + } case .snippet: self.init(enum: "sample", subnodes: params, multiline: true) case .plugin: @@ -541,6 +575,280 @@ fileprivate extension SourceCodeFragment { self.init(enum: setting.kind.name, subnodes: params) } } + + /// Instantiates a SourceCodeFragment from a single ProductSetting. + init(from productSetting: ProductSetting, toolsVersion: ToolsVersion) { + switch productSetting { + case .bundleIdentifier(let value): + self.init(key: "bundleIdentifier", string: value) + case .teamIdentifier(let value): + self.init(key: "teamIdentifier", string: value) + case .displayVersion(let value): + self.init(key: "displayVersion", string: value) + case .bundleVersion(let value): + self.init(key: "bundleVersion", string: value) + case .iOSAppInfo(let value): + self.init(key: "iOSAppInfo", subnode: SourceCodeFragment(from: value, toolsVersion: toolsVersion)) + } + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo. + init(from appInfo: ProductSetting.IOSAppInfo, toolsVersion: ToolsVersion) { + var params: [SourceCodeFragment] = [] + if let appIcon = appInfo.appIcon { + switch appIcon { + case let .placeholder(icon): + params.append(SourceCodeFragment(key: "appIcon", enum: "placeholder", subnodes: [SourceCodeFragment(from: icon)])) + case let .asset(name): + if toolsVersion < .v5_6 { + params.append(SourceCodeFragment(key: "iconAssetName", string: "\(name)")) + } + else { + params.append(SourceCodeFragment(key: "appIcon", enum: "asset", string: "\(name)")) + } + } + } + if let accentColor = appInfo.accentColor { + switch accentColor { + case let .presetColor(presetColor): + params.append(SourceCodeFragment(key: "accentColor", enum: "presetColor", subnodes: [SourceCodeFragment(from: presetColor)])) + case let .asset(name): + if toolsVersion < .v5_6 { + params.append(SourceCodeFragment(key: "accentColorAssetName", string: "\(name)")) + } + else { + params.append(SourceCodeFragment(key: "accentColor", enum: "asset", string: "\(name)")) + } + } + } + params.append(SourceCodeFragment(key: "supportedDeviceFamilies", subnodes: appInfo.supportedDeviceFamilies.map{ + SourceCodeFragment(from: $0) + })) + params.append(SourceCodeFragment(key: "supportedInterfaceOrientations", subnodes: appInfo.supportedInterfaceOrientations.map{ SourceCodeFragment(from: $0) + })) + if !appInfo.capabilities.isEmpty { + params.append(SourceCodeFragment(key: "capabilities", subnodes: appInfo.capabilities.map{ SourceCodeFragment(from: $0) })) + } + if let appCategory = appInfo.appCategory { + params.append(SourceCodeFragment(subnode: SourceCodeFragment(from: appCategory))) + } + if let additionalInfoPlistContentFilePath = appInfo.additionalInfoPlistContentFilePath { + params.append(SourceCodeFragment(key: "additionalInfoPlistContentFilePath", string: additionalInfoPlistContentFilePath)) + } + self.init(enum: "init", subnodes: params, multiline: true) + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.AppIcon.PlaceholderIcon. + init(from placeholderIcon: ProductSetting.IOSAppInfo.AppIcon.PlaceholderIcon) { + self.init(key: "icon", enum: placeholderIcon.rawValue) + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.AccentColor.PresetColor. + init(from presetColor: ProductSetting.IOSAppInfo.AccentColor.PresetColor) { + self.init(enum: presetColor.rawValue) + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.DeviceFamily. + init(from deviceFamily: ProductSetting.IOSAppInfo.DeviceFamily) { + self.init(enum: deviceFamily.rawValue) + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.DeviceFamilyCondition. + init(from deviceFamilyCondition: ProductSetting.IOSAppInfo.DeviceFamilyCondition) { + let deviceFamilyNodes = deviceFamilyCondition.deviceFamilies.map{ SourceCodeFragment(from: $0) } + let deviceFamiliesList = SourceCodeFragment(key: "deviceFamilies", subnodes: deviceFamilyNodes, multiline: false) + self.init(enum: "when", subnodes: [deviceFamiliesList]) + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.InterfaceOrientation. + init(from orientation: ProductSetting.IOSAppInfo.InterfaceOrientation) { + switch orientation { + case .portrait(let condition): + self.init(enum: "portrait", subnodes: condition.map{ [SourceCodeFragment(from: $0)] }) + case .portraitUpsideDown(let condition): + self.init(enum: "portraitUpsideDown", subnodes: condition.map{ [SourceCodeFragment(from: $0)] }) + case .landscapeLeft(let condition): + self.init(enum: "landscapeLeft", subnodes: condition.map{ [SourceCodeFragment(from: $0)] }) + case .landscapeRight(let condition): + self.init(enum: "landscapeRight", subnodes: condition.map{ [SourceCodeFragment(from: $0)] }) + } + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.Capability. + init(from capability: ProductSetting.IOSAppInfo.Capability) { + var params: [SourceCodeFragment] = [] + if let purposeString = capability.purposeString { + params.append(SourceCodeFragment(key: "purposeString", string: purposeString)) + } + if let configuration = capability.appTransportSecurityConfiguration { + params.append(SourceCodeFragment(key: "configuration", subnode: .init(from: configuration))) + } + if let bonjourServiceTypes = capability.bonjourServiceTypes { + params.append(SourceCodeFragment(key: "bonjourServiceTypes", strings: bonjourServiceTypes)) + } + if let fileAccessLocation = capability.fileAccessLocation { + params.append(SourceCodeFragment(enum: fileAccessLocation)) + } + if let fileAccessMode = capability.fileAccessMode { + params.append(SourceCodeFragment(key: "mode", enum: fileAccessMode)) + } + + if let condition = capability.condition { + params.append(SourceCodeFragment(from: condition)) + } + self.init(enum: capability.purpose, subnodes: params) + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration. + init(from configuration: ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration) { + var params: [SourceCodeFragment] = [] + if let allowsArbitraryLoadsInWebContent = configuration.allowsArbitraryLoadsInWebContent { + params.append(SourceCodeFragment(key: "allowsArbitraryLoadsInWebContent", boolean: allowsArbitraryLoadsInWebContent)) + } + if let allowsArbitraryLoadsForMedia = configuration.allowsArbitraryLoadsForMedia { + params.append(SourceCodeFragment(key: "allowsArbitraryLoadsForMedia", boolean: allowsArbitraryLoadsForMedia)) + } + if let allowsLocalNetworking = configuration.allowsLocalNetworking { + params.append(SourceCodeFragment(key: "allowsLocalNetworking", boolean: allowsLocalNetworking)) + } + if let exceptionDomains = configuration.exceptionDomains { + let subnodes = exceptionDomains.map{ SourceCodeFragment(from: $0) } + params.append(SourceCodeFragment(key: "exceptionDomains", subnodes: subnodes)) + } + if let pinnedDomains = configuration.pinnedDomains { + let subnodes = pinnedDomains.map{ SourceCodeFragment(from: $0) } + params.append(SourceCodeFragment(key: "pinnedDomains", subnodes: subnodes)) + } + self.init(enum: "init", subnodes: params, multiline: true) + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.ExceptionDomain. + init(from domain: ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.ExceptionDomain) { + var params: [SourceCodeFragment] = [] + params.append(SourceCodeFragment(key: "domainName", string: domain.domainName)) + if let includesSubdomains = domain.includesSubdomains { + params.append(SourceCodeFragment(key: "includesSubdomains", boolean: includesSubdomains)) + } + if let exceptionAllowsInsecureHTTPLoads = domain.exceptionAllowsInsecureHTTPLoads { + params.append(SourceCodeFragment(key: "exceptionAllowsInsecureHTTPLoads", boolean: exceptionAllowsInsecureHTTPLoads)) + } + if let exceptionMinimumTLSVersion = domain.exceptionMinimumTLSVersion { + params.append(SourceCodeFragment(key: "exceptionMinimumTLSVersion", string: exceptionMinimumTLSVersion)) + } + if let exceptionRequiresForwardSecrecy = domain.exceptionRequiresForwardSecrecy { + params.append(SourceCodeFragment(key: "exceptionRequiresForwardSecrecy", boolean: exceptionRequiresForwardSecrecy)) + } + if let requiresCertificateTransparency = domain.requiresCertificateTransparency { + params.append(SourceCodeFragment(key: "requiresCertificateTransparency", boolean: requiresCertificateTransparency)) + } + self.init(enum: "init", subnodes: params, multiline: true) + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.ExceptionDomain. + init(from domain: ProductSetting.IOSAppInfo.AppTransportSecurityConfiguration.PinnedDomain) { + var params: [SourceCodeFragment] = [] + params.append(SourceCodeFragment(key: "domainName", string: domain.domainName)) + if let includesSubdomains = domain.includesSubdomains { + params.append(SourceCodeFragment(key: "includesSubdomains", boolean: includesSubdomains)) + } + if let pinnedCAIdentities = domain.pinnedCAIdentities { + let subnodes = pinnedCAIdentities.map{ SourceCodeFragment(stringPairs: $0.sorted{ $0.key < $1.key }.map{ ($0.key, $0.value) }) } + params.append(SourceCodeFragment(key: "pinnedCAIdentities", subnodes: subnodes)) + } + if let pinnedLeafIdentities = domain.pinnedLeafIdentities { + let subnodes = pinnedLeafIdentities.map{ SourceCodeFragment(stringPairs: $0.sorted{ $0.key < $1.key }.map{ ($0.key, $0.value) }) } + params.append(SourceCodeFragment(key: "pinnedLeafIdentities", subnodes: subnodes)) + } + self.init(enum: "init", subnodes: params, multiline: true) + } + + /// Instantiates a SourceCodeFragment from a single ProductSetting.IOSAppInfo.AppCategory. + init(from appCategory: ProductSetting.IOSAppInfo.AppCategory) { + switch appCategory.rawValue { + case "public.app-category.action-games": + self.init(key: "appCategory", enum: "actionGames") + case "public.app-category.adventure-games": + self.init(key: "appCategory", enum: "adventureGames") + case "public.app-category.arcade-games": + self.init(key: "appCategory", enum: "arcadeGames") + case "public.app-category.board-games": + self.init(key: "appCategory", enum: "boardGames") + case "public.app-category.business": + self.init(key: "appCategory", enum: "business") + case "public.app-category.card-games": + self.init(key: "appCategory", enum: "cardGames") + case "public.app-category.casino-games": + self.init(key: "appCategory", enum: "casinoGames") + case "public.app-category.developer-tools": + self.init(key: "appCategory", enum: "developerTools") + case "public.app-category.dice-games": + self.init(key: "appCategory", enum: "diceGames") + case "public.app-category.education": + self.init(key: "appCategory", enum: "education") + case "public.app-category.educational-games": + self.init(key: "appCategory", enum: "educationalGames") + case "public.app-category.entertainment": + self.init(key: "appCategory", enum: "entertainment") + case "public.app-category.family-games": + self.init(key: "appCategory", enum: "familyGames") + case "public.app-category.finance": + self.init(key: "appCategory", enum: "finance") + case "public.app-category.games": + self.init(key: "appCategory", enum: "games") + case "public.app-category.graphics-design": + self.init(key: "appCategory", enum: "graphicsDesign") + case "public.app-category.healthcare-fitness": + self.init(key: "appCategory", enum: "healthcareFitness") + case "public.app-category.kids-games": + self.init(key: "appCategory", enum: "kidsGames") + case "public.app-category.lifestyle": + self.init(key: "appCategory", enum: "lifestyle") + case "public.app-category.medical": + self.init(key: "appCategory", enum: "medical") + case "public.app-category.music": + self.init(key: "appCategory", enum: "music") + case "public.app-category.music-games": + self.init(key: "appCategory", enum: "musicGames") + case "public.app-category.news": + self.init(key: "appCategory", enum: "news") + case "public.app-category.photography": + self.init(key: "appCategory", enum: "photography") + case "public.app-category.productivity": + self.init(key: "appCategory", enum: "productivity") + case "public.app-category.puzzle-games": + self.init(key: "appCategory", enum: "puzzleGames") + case "public.app-category.racing-games": + self.init(key: "appCategory", enum: "racingGames") + case "public.app-category.reference": + self.init(key: "appCategory", enum: "reference") + case "public.app-category.role-playing-games": + self.init(key: "appCategory", enum: "rolePlayingGames") + case "public.app-category.simulation-games": + self.init(key: "appCategory", enum: "simulationGames") + case "public.app-category.social-networking": + self.init(key: "appCategory", enum: "socialNetworking") + case "public.app-category.sports": + self.init(key: "appCategory", enum: "sports") + case "public.app-category.sports-games": + self.init(key: "appCategory", enum: "sportsGames") + case "public.app-category.strategy-games": + self.init(key: "appCategory", enum: "strategyGames") + case "public.app-category.travel": + self.init(key: "appCategory", enum: "travel") + case "public.app-category.trivia-games": + self.init(key: "appCategory", enum: "triviaGames") + case "public.app-category.utilities": + self.init(key: "appCategory", enum: "utilities") + case "public.app-category.video": + self.init(key: "appCategory", enum: "video") + case "public.app-category.weather": + self.init(key: "appCategory", enum: "weather") + case "public.app-category.word-games": + self.init(key: "appCategory", enum: "wordGames") + default: + self.init(key: "appCategory", string: appCategory.rawValue) + } + } } @@ -593,6 +901,13 @@ public extension SourceCodeFragment { self.init(prefix, delimiters: .brackets, multiline: multiline, subnodes: subnodes) } + /// Initializes a SourceCodeFragment for a string map in a generated manifest. + init(key: String? = nil, stringPairs: [(String, String)], multiline: Bool = false) { + let prefix = key.map{ $0 + ": " } ?? "" + let subnodes = stringPairs.isEmpty ? [SourceCodeFragment(":")] : stringPairs.map{ SourceCodeFragment($0.quotedForPackageManifest + ": " + $1.quotedForPackageManifest) } + self.init(prefix, delimiters: .brackets, multiline: multiline, subnodes: subnodes) + } + /// Initializes a SourceCodeFragment for a node in a generated manifest. init(key: String? = nil, subnode: SourceCodeFragment) { let prefix = key.map{ $0 + ": " } ?? "" diff --git a/Sources/PackagePlugin/Context.swift b/Sources/PackagePlugin/Context.swift index c713ea25abc..1dc25a56ef7 100644 --- a/Sources/PackagePlugin/Context.swift +++ b/Sources/PackagePlugin/Context.swift @@ -107,5 +107,11 @@ public struct PluginContext { /// Full path of the built or provided tool in the file system. @available(_PackageDescription, introduced: 6.0) public let url: URL + + @_spi(PackagePluginInternal) public init(name: String, path: Path, url: URL) { + self.name = name + self.path = path + self.url = url + } } } diff --git a/Sources/PackagePlugin/Errors.swift b/Sources/PackagePlugin/Errors.swift index b2c0155a26d..2d7a08c21f5 100644 --- a/Sources/PackagePlugin/Errors.swift +++ b/Sources/PackagePlugin/Errors.swift @@ -43,6 +43,14 @@ extension PluginContextError: CustomStringConvertible { public enum PluginDeserializationError: Error { /// The input JSON is malformed in some way; the message provides more details. case malformedInputJSON(_ message: String) + /// The plugin doesn't support Xcode (it doesn't link against XcodeProjectPlugin). + case missingXcodeProjectPluginSupport + /// The plugin doesn't conform to an expected specialization of the BuildToolPlugin protocol. + case missingBuildToolPluginProtocolConformance(protocolName: String) + /// The plugin doesn't conform to an expected specialization of the CommandPlugin protocol. + case missingCommandPluginProtocolConformance(protocolName: String) + /// An internal error of some kind; the message provides more details. + case internalError(_ message: String) } extension PluginDeserializationError: CustomStringConvertible { @@ -50,6 +58,14 @@ extension PluginDeserializationError: CustomStringConvertible { switch self { case .malformedInputJSON(let message): return "Malformed input JSON: \(message)" + case .missingXcodeProjectPluginSupport: + return "Plugin doesn't support Xcode projects (it doesn't use the XcodeProjectPlugin library)" + case .missingBuildToolPluginProtocolConformance(let protocolName): + return "Plugin is declared with the `buildTool` capability, but doesn't conform to the `\(protocolName)` protocol" + case .missingCommandPluginProtocolConformance(let protocolName): + return "Plugin is declared with the `command` capability, but doesn't conform to the `\(protocolName)` protocol" + case .internalError(let message): + return "Internal error: \(message)" } } } diff --git a/Sources/PackagePlugin/PackageModel.swift b/Sources/PackagePlugin/PackageModel.swift index 5f131d76ddf..88d229ca26f 100644 --- a/Sources/PackagePlugin/PackageModel.swift +++ b/Sources/PackagePlugin/PackageModel.swift @@ -81,6 +81,12 @@ public struct ToolsVersion { /// The patch version. public let patch: Int + + @_spi(PackagePluginInternal) public init(major: Int, minor: Int, patch: Int) { + self.major = major + self.minor = minor + self.patch = patch + } } /// Represents a resolved dependency of a package on another package. This is a @@ -474,7 +480,7 @@ public struct SystemLibraryTarget: Target { public struct FileList { private var files: [File] - init(_ files: [File]) { + @_spi(PackagePluginInternal) public init(_ files: [File]) { self.files = files } } @@ -510,7 +516,9 @@ extension FileList: RandomAccessCollection { public struct File { /// The path of the file. @available(_PackageDescription, deprecated: 6.0, renamed: "url") - public let path: Path + public var path: Path { + return try! Path(url: url) + } /// The path of the file. @available(_PackageDescription, introduced: 6.0) @@ -518,6 +526,11 @@ public struct File { /// File type, as determined by SwiftPM. public let type: FileType + + @_spi(PackagePluginInternal) public init(url: URL, type: FileType) { + self.url = url + self.type = type + } } /// Provides information about the type of a file. Any future cases will @@ -536,3 +549,33 @@ public enum FileType { /// A file not covered by any other rule. case unknown } + +/// Provides information about a list of paths. The order is not defined +/// but is guaranteed to be stable. This allows the implementation to be +/// more efficient than a static path list. +public struct PathList { + private var paths: [URL] + + @_spi(PackagePluginInternal) public init(_ paths: [URL]) { + self.paths = paths + } +} +extension PathList: Sequence { + public struct Iterator: IteratorProtocol { + private var paths: ArraySlice + fileprivate init(paths: ArraySlice) { + self.paths = paths + } + mutating public func next() -> Path? { + guard let nextInfo = self.paths.popFirst() else { + return nil + } + return nextInfo + } + } + public func makeIterator() -> Iterator { + // FIXME: This iterator should be converted to URLs, too, but that doesn't seem to be possible without breaking source compatibility + return Iterator(paths: ArraySlice(self.paths.map { try! Path(url: $0) })) + } +} + diff --git a/Sources/PackagePlugin/Plugin.swift b/Sources/PackagePlugin/Plugin.swift index 8e5d3f1a6a5..232543b6650 100644 --- a/Sources/PackagePlugin/Plugin.swift +++ b/Sources/PackagePlugin/Plugin.swift @@ -13,6 +13,7 @@ import Foundation #if os(Windows) @_implementationOnly import ucrt +@_implementationOnly import WinSDK internal func dup(_ fd: CInt) -> CInt { return _dup(fd) @@ -192,8 +193,7 @@ extension Plugin { // Check that the plugin implements the appropriate protocol // for its declared `.buildTool` capability. guard let plugin = plugin as? BuildToolPlugin else { - throw PluginDeserializationError.malformedInputJSON( - "Plugin declared with `buildTool` capability but doesn't conform to `BuildToolPlugin` protocol") + throw PluginDeserializationError.missingBuildToolPluginProtocolConformance(protocolName: "BuildToolPlugin") } // Invoke the plugin to create build commands for the target. @@ -235,6 +235,83 @@ extension Plugin { // Exit with a zero exit code to indicate success. exit(0) + case .createXcodeProjectBuildToolCommands(let wireInput, let rootProjectId, let targetId, let generatedSources, let generatedResources): + // Instantiate the plugin (for now without parameters, as described + // above). + let plugin = self.init() + + // Check that the plugin implements the appropriate protocol + // for its declared `.buildTool` capability. + guard let plugin = plugin as? BuildToolPlugin else { + throw PluginDeserializationError.missingBuildToolPluginProtocolConformance(protocolName: "BuildToolPlugin") + } + + // Deserialize the context from the wire input structures, and create a record for us to pass to the XcodeProjectPlugin library. + let record: XcodeProjectPluginInvocationRecord + do { + var deserializer = PluginContextDeserializer(wireInput) + let xcodeProject = try deserializer.xcodeProject(for: rootProjectId) + let xcodeTarget = try deserializer.xcodeTarget( + for: targetId, + pluginGeneratedSources: try generatedSources.map { try deserializer.url(for: $0) }, + pluginGeneratedResources: try generatedResources.map { try deserializer.url(for: $0) } + ) + let pluginWorkDirectory = try deserializer.url(for: wireInput.pluginWorkDirId) + let toolSearchDirectories = try wireInput.toolSearchDirIds.map { + try deserializer.url(for: $0) + } + let accessibleTools = try wireInput.accessibleTools.mapValues { (tool: HostToPluginMessage.InputContext.Tool) -> (URL, [String]?) in + let path = try deserializer.url(for: tool.path) + return (path, tool.triples) + } + record = XcodeProjectPluginInvocationRecord( + plugin: plugin, + xcodeProject: xcodeProject, + xcodeTarget: xcodeTarget, + pluginWorkDirectory: pluginWorkDirectory, + accessibleTools: accessibleTools, + toolSearchDirectories: toolSearchDirectories) + } + catch { + internalError("Couldn’t deserialize input from host: \(error).") + } + + try callEntryPoint(record, "call_XcodeProjectPlugin_build_command_creation_entry_point") + + // Send each of the generated commands to the host. + for command in record.generatedCommands { + switch command { + + case let .buildCommand(name, exec, args, env, inputs, outputs): + let command = PluginToHostMessage.CommandConfiguration( + displayName: name, + executable: exec, + arguments: args, + environment: env, + workingDirectory: nil) + let message = PluginToHostMessage.defineBuildCommand( + configuration: command, + inputFiles: inputs, + outputFiles: outputs) + try pluginHostConnection.sendMessage(message) + + case let .prebuildCommand(name, exec, args, env, outdir): + let command = PluginToHostMessage.CommandConfiguration( + displayName: name, + executable: exec, + arguments: args, + environment: env, + workingDirectory: nil) + let message = PluginToHostMessage.definePrebuildCommand( + configuration: command, + outputFilesDirectory: outdir) + try pluginHostConnection.sendMessage(message) + } + } + + // Exit with a zero exit code to indicate success. + exit(0) + case .performCommand(let wireInput, let rootPackageId, let arguments): // Deserialize the context from the wire input structures. The root // package is the one we'll set the context's `package` property to. @@ -269,8 +346,7 @@ extension Plugin { // Check that the plugin implements the appropriate protocol // for its declared `.command` capability. guard let plugin = plugin as? CommandPlugin else { - throw PluginDeserializationError.malformedInputJSON( - "Plugin declared with `command` capability but doesn't conform to `CommandPlugin` protocol") + throw PluginDeserializationError.missingCommandPluginProtocolConformance(protocolName: "CommandPlugin") } // Invoke the plugin to perform its custom logic. @@ -278,7 +354,48 @@ extension Plugin { // Exit with a zero exit code to indicate success. exit(0) + + case .performXcodeProjectCommand(let wireInput, let rootProjectId, let arguments): + // Instantiate the plugin (for now without parameters, as described + // above). + let plugin = self.init() + + // Check that the plugin implements the appropriate protocol + // for its declared `.command` capability. + guard let plugin = plugin as? CommandPlugin else { + throw PluginDeserializationError.missingCommandPluginProtocolConformance(protocolName: "CommandPlugin") + } + // Deserialize the context from the wire input structures, and create a record for us to pass to the XcodeProjectPlugin library. + let record: XcodeProjectPluginInvocationRecord + do { + var deserializer = PluginContextDeserializer(wireInput) + let xcodeProject = try deserializer.xcodeProject(for: rootProjectId) + let pluginWorkDirectory = try deserializer.url(for: wireInput.pluginWorkDirId) + let toolSearchDirectories = try wireInput.toolSearchDirIds.map { + try deserializer.url(for: $0) + } + let accessibleTools = try wireInput.accessibleTools.mapValues { (tool: HostToPluginMessage.InputContext.Tool) -> (URL, [String]?) in + let path = try deserializer.url(for: tool.path) + return (path, tool.triples) + } + record = XcodeProjectPluginInvocationRecord( + plugin: plugin, + xcodeProject: xcodeProject, + pluginWorkDirectory: pluginWorkDirectory, + accessibleTools: accessibleTools, + toolSearchDirectories: toolSearchDirectories, + arguments: arguments) + } + catch { + internalError("Couldn’t deserialize input from host: \(error).") + } + + try callEntryPoint(record, "call_XcodeProjectPlugin_custom_command_entry_point") + + // Exit with a zero exit code to indicate success. + exit(0) + default: internalError("unexpected top-level message \(message)") } @@ -301,6 +418,93 @@ extension Plugin { } } +@_spi(PackagePluginInternal) public class XcodeProjectPluginInvocationRecord { + public let plugin: Plugin + public let xcodeProject: XcodeProject + public let xcodeTarget: XcodeTarget? + @available(_PackageDescription, introduced: 5.11) + public let pluginWorkDirectoryURL: URL + @available(_PackageDescription, introduced: 5.11) + public let accessibleToolsByURL: [String: (path: URL, triples: [String]?)] + @available(_PackageDescription, introduced: 5.11) + public let toolSearchDirectoryURLs: [URL] + public let arguments: [String] + public var generatedCommands: [Command] = [] + + @available(_PackageDescription, deprecated: 5.11) + public var pluginWorkDirectory: Path { + return try! Path(url: self.pluginWorkDirectoryURL) + } + @available(_PackageDescription, deprecated: 5.11) + public var accessibleTools: [String: (path: Path, triples: [String]?)] { + var result = [String: (path: Path, triples: [String]?)]() + self.accessibleToolsByURL.forEach { + result[$0.key] = (try! Path(url: $0.value.path), $0.value.triples) + } + return result + } + @available(_PackageDescription, deprecated: 5.11) + public var toolSearchDirectories: [Path] { + return self.toolSearchDirectoryURLs.map { try! Path(url: $0) } + } + + internal init( + plugin: Plugin, + xcodeProject: XcodeProject, + xcodeTarget: XcodeTarget? = .none, + pluginWorkDirectory: URL, + accessibleTools: [String: (path: URL, triples: [String]?)], + toolSearchDirectories: [URL], + arguments: [String] = [] + ) { + self.plugin = plugin + self.xcodeProject = xcodeProject + self.xcodeTarget = xcodeTarget + self.pluginWorkDirectoryURL = pluginWorkDirectory + self.accessibleToolsByURL = accessibleTools + self.toolSearchDirectoryURLs = toolSearchDirectories + self.arguments = arguments + self.generatedCommands = [] + } + public struct XcodeProject { + public var id: String + public var displayName: String + @available(_PackageDescription, deprecated: 5.11) + public var directoryPath: Path { + return try! Path(url: directoryPathURL) + } + @available(_PackageDescription, introduced: 5.11) + public var directoryPathURL: URL + public var filePaths: PathList + public var targets: [XcodeTarget] + } + public struct XcodeTarget { + public var id: String + public var displayName: String + public var product: Product? + public var inputFiles: FileList + public struct Product { + public var name: String + public var kind: Kind + public enum Kind { + case application + case executable + case framework + case library + case other(String) + } + } + + /// Paths of any sources generated by other plugins that have been applied to the given target before the plugin currently being executed. + @available(_PackageDescription, introduced: 5.11) + public let pluginGeneratedSources: [URL] + + /// Paths of any resources generated by other plugins that have been applied to the given target before the plugin currently being executed. + @available(_PackageDescription, introduced: 5.11) + public let pluginGeneratedResources: [URL] + } +} + /// Message channel for bidirectional communication with the plugin host. internal fileprivate(set) var pluginHostConnection: PluginHostConnection! @@ -352,3 +556,85 @@ internal struct MessageConnection where TX: Encodable, RX: Decodable { case truncatedPayload } } + +fileprivate func callEntryPoint(_ record: XcodeProjectPluginInvocationRecord, _ functionName: String) throws { + #if !canImport(Darwin) + // Workaround for a compiler crash presumably related to Objective-C bridging on non-Darwin platforms (rdar://130826719&136043295) + typealias CallerFuncType = @convention(c) (UnsafeRawPointer) -> Any + #else + typealias CallerFuncType = @convention(c) (UnsafeRawPointer) -> (any Error)? + #endif + + // Find the trampoline for the type of custom command (it's expected to be in the add-on library). + guard let callerFunc: CallerFuncType = try Library.lookup(Library.open(), functionName) else { + throw PluginDeserializationError.missingXcodeProjectPluginSupport + } + + // The caller function is expected to take a pointer to a XcodeProjectPluginInvocationRecord. It is expected to return nil on success or an error on failure, as there is no way of throwing form a C function. + let recordPtr = UnsafeRawPointer(Unmanaged.passUnretained(record).toOpaque()) + #if !canImport(Darwin) + // Workaround for a compiler crash presumably related to Objective-C bridging on non-Darwin platforms (rdar://130826719&136043295) + /*if let error = callerFunc(recordPtr) as! (any Error)? { + throw error + }*/ + fatalError("FIXME: Compiler crashes when trying to compile a call to callerFunc") + #else + if let error = callerFunc(recordPtr) { + throw error + } + #endif +} + +fileprivate enum Library: Sendable { + @_alwaysEmitIntoClient + public static func open() throws -> LibraryHandle { + #if os(Windows) + guard let handle = GetModuleHandleW(nil) else { + throw LibraryOpenError(message: "GetModuleHandleW returned \(GetLastError())") + } + return LibraryHandle(rawValue: handle) + #else + guard let handle = dlopen(nil, RTLD_NOW | RTLD_LOCAL) else { + throw LibraryOpenError(message: String(cString: dlerror())) + } + return LibraryHandle(rawValue: handle) + #endif + } + + public static func lookup(_ handle: LibraryHandle, _ symbol: String) -> T? { + #if os(Windows) + guard let ptr = GetProcAddress(handle.rawValue, symbol) else { return nil } + #else + guard let ptr = dlsym(handle.rawValue, symbol) else { return nil } + #endif + return unsafeBitCast(ptr, to: T.self) + } +} + +fileprivate struct LibraryOpenError: Error, CustomStringConvertible, Sendable { + public let message: String + + public var description: String { + message + } + + @usableFromInline + internal init(message: String) { + self.message = message + } +} + +fileprivate struct LibraryHandle: @unchecked Sendable { + #if os(Windows) + @usableFromInline typealias PlatformHandle = HMODULE + #else + @usableFromInline typealias PlatformHandle = UnsafeMutableRawPointer + #endif + + fileprivate let rawValue: PlatformHandle + + @usableFromInline + internal init(rawValue: PlatformHandle) { + self.rawValue = rawValue + } +} diff --git a/Sources/PackagePlugin/PluginContextDeserializer.swift b/Sources/PackagePlugin/PluginContextDeserializer.swift index 5b10783c874..1593d30f745 100644 --- a/Sources/PackagePlugin/PluginContextDeserializer.swift +++ b/Sources/PackagePlugin/PluginContextDeserializer.swift @@ -25,6 +25,8 @@ internal struct PluginContextDeserializer { var packagesById: [WireInput.Package.Id: Package] = [:] var productsById: [WireInput.Product.Id: Product] = [:] var targetsById: [WireInput.Target.Id: Target] = [:] + var xcodeProjectsById: [WireInput.XcodeProject.Id: XcodeProjectPluginInvocationRecord.XcodeProject] = [:] + var xcodeTargetsById: [WireInput.XcodeTarget.Id: XcodeProjectPluginInvocationRecord.XcodeTarget] = [:] /// Initializes the deserializer with the given wire input. init(_ input: WireInput) { @@ -102,7 +104,7 @@ internal struct PluginContextDeserializer { case .unknown: type = .unknown } - return try File(path: Path(url: path), url: path, type: type) + return File(url: path, type: type) }) target = try SwiftSourceModuleTarget( id: String(id), @@ -135,7 +137,7 @@ internal struct PluginContextDeserializer { case .unknown: type = .unknown } - return try File(path: Path(url: path), url: path, type: type) + return File(url: path, type: type) }) target = try ClangSourceModuleTarget( id: String(id), @@ -285,6 +287,87 @@ internal struct PluginContextDeserializer { packagesById[id] = package return package } + + /// Returns the `XcodeTarget` that corresponds to the given ID (a small integer), + /// or throws an error if the ID is invalid. The product is deserialized on- + /// demand if it hasn't already been deserialized. + mutating func xcodeTarget(for id: WireInput.XcodeTarget.Id, pluginGeneratedSources: [URL] = [], pluginGeneratedResources: [URL] = []) throws -> XcodeProjectPluginInvocationRecord.XcodeTarget { + if let xcodeTarget = xcodeTargetsById[id], + xcodeTarget.pluginGeneratedSources.count == pluginGeneratedSources.count, + xcodeTarget.pluginGeneratedResources.count == pluginGeneratedResources.count { + return xcodeTarget + } + guard id < wireInput.xcodeTargets.count else { + throw PluginDeserializationError.malformedInputJSON("invalid Xcode target id (\(id))") + } + + let wireXcodeTarget = wireInput.xcodeTargets[id] + let product: XcodeProjectPluginInvocationRecord.XcodeTarget.Product? = wireXcodeTarget.product.map { + let kind: XcodeProjectPluginInvocationRecord.XcodeTarget.Product.Kind + switch $0.kind { + case .application: + kind = .application + case .executable: + kind = .executable + case .framework: + kind = .framework + case .library: + kind = .library + case .other(let ident): + kind = .other(ident) + } + return .init(name: $0.name, kind: kind) + } + let inputFiles = FileList(try wireXcodeTarget.inputFiles.map { + let path = try self.url(for: $0.basePathId).appendingPathComponent($0.name) + let type: FileType + switch $0.type { + case .source: + type = .source + case .header: + type = .header + case .resource: + type = .resource + case .unknown: + type = .unknown + } + return .init(url: path, type: type) + }) + let xcodeTarget = XcodeProjectPluginInvocationRecord.XcodeTarget( + id: String(id), + displayName: wireXcodeTarget.displayName, + product: product, + inputFiles: inputFiles, + pluginGeneratedSources: pluginGeneratedSources, + pluginGeneratedResources: pluginGeneratedResources + ) + + xcodeTargetsById[id] = xcodeTarget + return xcodeTarget + } + + /// Returns the `Package` that corresponds to the given ID (a small integer), + /// or throws an error if the ID is invalid. The package is deserialized on- + /// demand if it hasn't already been deserialized. + mutating func xcodeProject(for id: WireInput.XcodeProject.Id) throws -> XcodeProjectPluginInvocationRecord.XcodeProject { + if let xcodeProject = xcodeProjectsById[id] { return xcodeProject } + guard id < wireInput.xcodeProjects.count else { + throw PluginDeserializationError.malformedInputJSON("invalid Xcode project id (\(id))") } + + let wireXcodeProject = wireInput.xcodeProjects[id] + let directoryPath = try self.url(for: wireXcodeProject.directoryPathId) + let filePaths = PathList(try wireXcodeProject.urlIds.map{ try self.url(for: $0) }) + let targets = try wireXcodeProject.targetIds.map { try self.xcodeTarget(for: $0) } + let xcodeProject = XcodeProjectPluginInvocationRecord.XcodeProject( + id: String(id), + displayName: wireXcodeProject.displayName, + directoryPathURL: directoryPath, + filePaths: filePaths, + targets: targets) + + xcodeProjectsById[id] = xcodeProject + return xcodeProject + } } fileprivate extension ModuleKind { diff --git a/Sources/PackagePlugin/PluginMessages.swift b/Sources/PackagePlugin/PluginMessages.swift index 156cb7d09de..bd1e96850e6 100644 --- a/Sources/PackagePlugin/PluginMessages.swift +++ b/Sources/PackagePlugin/PluginMessages.swift @@ -24,14 +24,28 @@ enum HostToPluginMessage: Codable { pluginGeneratedResources: [InputContext.URL.Id] ) + /// The host requests that the plugin create build commands (corresponding to a `.buildTool` capability) for a target in the package graph. + case createXcodeProjectBuildToolCommands( + context: InputContext, + rootProjectId: InputContext.XcodeProject.Id, + targetId: InputContext.XcodeTarget.Id, + pluginGeneratedSources: [InputContext.URL.Id], + pluginGeneratedResources: [InputContext.URL.Id] + ) + /// The host requests that the plugin perform a user command (corresponding to a `.command` capability) on a package in the graph. case performCommand(context: InputContext, rootPackageId: InputContext.Package.Id, arguments: [String]) + /// The host requests that the plugin perform a user command (corresponding to a `.command` capability) on a package in the graph. + case performXcodeProjectCommand(context: InputContext, rootProjectId: InputContext.XcodeProject.Id, arguments: [String]) + struct InputContext: Codable { let paths: [URL] let targets: [Target] let products: [Product] let packages: [Package] + let xcodeTargets: [XcodeTarget] + let xcodeProjects: [XcodeProject] let pluginWorkDirId: URL.Id let toolSearchDirIds: [URL.Id] let accessibleTools: [String: Tool] @@ -163,19 +177,6 @@ enum HostToPluginMessage: Codable { compilerFlags: [String], linkerFlags: [String]) - struct File: Codable { - let basePathId: URL.Id - let name: String - let type: FileType - - enum FileType: String, Codable { - case source - case header - case resource - case unknown - } - } - enum SourceModuleKind: String, Codable { case generic case executable @@ -195,6 +196,70 @@ enum HostToPluginMessage: Codable { } } } + + /// A typed file in the wire structure. All references to other entities are + /// their ID numbers. + struct File: Codable { + let basePathId: URL.Id + let name: String + let type: FileType + + enum FileType: String, Codable { + case source + case header + case resource + case unknown + } + } + + /// An Xcode project in the wire structure. All references to other entities are their ID numbers. + struct XcodeProject: Codable { + typealias Id = Int + let displayName: String + let directoryPathId: URL.Id + let dependencies: [Dependency] + let urlIds: [URL.Id] + let targetIds: [XcodeTarget.Id] + + /// A dependency on a package or project in the wire structure. All references to + /// other entities are ID numbers. + enum Dependency: Codable { + case package(Package.Id) + case xcodeProject(XcodeProject.Id) + } + } + + /// A target in the wire structure. All references to other entities are + /// their ID numbers. + struct XcodeTarget: Codable { + typealias Id = Int + let displayName: String + let product: XcodeProduct? + let dependencies: [Dependency] + let inputFiles: [File] + + /// A product in the wire structure. + struct XcodeProduct: Codable { + let name: String + let kind: Kind + public enum Kind: Codable { + case application + case executable + case framework + case library + case other(String) + } + } + + /// A dependency on either a target or a product in the wire structure. + /// All references to other entities are ID their numbers. + enum Dependency: Codable { + case target( + targetId: XcodeTarget.Id) + case product( + productId: Product.Id) + } + } } /// A response to a request to run a build operation. diff --git a/Sources/SPMBuildCore/CMakeLists.txt b/Sources/SPMBuildCore/CMakeLists.txt index 4268fedf93c..eaf92ddd125 100644 --- a/Sources/SPMBuildCore/CMakeLists.txt +++ b/Sources/SPMBuildCore/CMakeLists.txt @@ -26,7 +26,8 @@ add_library(SPMBuildCore CommandPluginResult.swift ResolvedPackage+Extensions.swift Triple+Extensions.swift - XCFrameworkMetadata.swift) + XCFrameworkMetadata.swift + XcodeProjectRepresentation.swift) # NOTE(compnerd) workaround for CMake not setting up include flags yet set_target_properties(SPMBuildCore PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/SPMBuildCore/Plugins/PluginContextSerializer.swift b/Sources/SPMBuildCore/Plugins/PluginContextSerializer.swift index c6e057a8610..abb2f6d67c5 100644 --- a/Sources/SPMBuildCore/Plugins/PluginContextSerializer.swift +++ b/Sources/SPMBuildCore/Plugins/PluginContextSerializer.swift @@ -35,6 +35,11 @@ internal struct PluginContextSerializer { var packages: [WireInput.Package] = [] var packagesToWireIDs: [ResolvedPackage.ID: WireInput.Package.Id] = [:] + var xcodeTargets: [WireInput.XcodeTarget] = [] + var xcodeTargetsToIds: [XcodeProjectRepresentation.Target: WireInput.XcodeTarget.Id] = [:] + var xcodeProjects: [WireInput.XcodeProject] = [] + var xcodeProjectsToIds: [XcodeProjectRepresentation: WireInput.XcodeProject.Id] = [:] + /// Adds a path to the serialized structure, if it isn't already there. /// Either way, this function returns the path's wire ID. mutating func serialize(path: AbsolutePath) throws -> WireInput.URL.Id { @@ -75,7 +80,7 @@ internal struct PluginContextSerializer { if let id = targetsToWireIDs[target.id] { return id } // Construct the FileList - var targetFiles: [WireInput.Target.TargetInfo.File] = [] + var targetFiles: [WireInput.File] = [] targetFiles.append(contentsOf: try target.underlying.sources.paths.map { .init(basePathId: try serialize(path: $0.parentDirectory), name: $0.basename, type: .source) }) @@ -280,6 +285,47 @@ internal struct PluginContextSerializer { packagesToWireIDs[package.id] = id return id } + + // Adds an Xcode target to the serialized structure, if it isn't already there and if it is of a kind that should be passed to the plugin. If so, this function returns the target's wire ID. If not, it returns nil. + mutating func serialize(xcodeTarget: XcodeProjectRepresentation.Target) throws -> WireInput.XcodeTarget.Id? { + // If we've already seen the target, just return the wire ID we already assigned to it. + if let id = xcodeTargetsToIds[xcodeTarget] { return id } + + // Create the list of source files. + var inputFiles: [WireInput.File] = [] + inputFiles.append(contentsOf: try xcodeTarget.inputFiles.map { + .init(basePathId: try serialize(path: $0.path.parentDirectory), name: $0.path.basename, type: .init($0.role)) + }) + + // Assign the next wire ID to the target, and append a serialized XcodeProject record. + let id = xcodeTargets.count + xcodeTargets.append(.init( + displayName: xcodeTarget.displayName, + product: xcodeTarget.product.map{ .init(name: $0.name, kind: .init($0.kind)) }, + dependencies: [], + inputFiles: inputFiles)) + xcodeTargetsToIds[xcodeTarget] = id + return id + } + + + // Adds an Xcode project to the serialized structure, if it isn't already there. + // Either way, this function returns the project's wire ID. + mutating func serialize(xcodeProject: XcodeProjectRepresentation) throws -> WireInput.XcodeProject.Id { + // If we've already seen the project, just return the wire ID we already assigned to it. + if let id = xcodeProjectsToIds[xcodeProject] { return id } + + // Assign the next wire ID to the project, and append a serialized XcodeProject record. + let id = xcodeProjects.count + xcodeProjects.append(.init( + displayName: xcodeProject.displayName, + directoryPathId: try serialize(path: xcodeProject.directoryPath), + dependencies: [], + urlIds: try xcodeProject.filePaths.map { try serialize(path: $0) }, + targetIds: try xcodeProject.targets.compactMap{ try serialize(xcodeTarget: $0) })) + xcodeProjectsToIds[xcodeProject] = id + return id + } } fileprivate extension WireInput.Target.TargetInfo.SourceModuleKind { @@ -300,3 +346,35 @@ fileprivate extension WireInput.Target.TargetInfo.SourceModuleKind { } } } + +fileprivate extension WireInput.File.FileType { + init(_ role: XcodeProjectRepresentation.Target.InputFile.Role) { + switch role { + case .source: + self = .source + case .header: + self = .header + case .resource: + self = .resource + case .unknown: + self = .unknown + } + } +} + +fileprivate extension WireInput.XcodeTarget.XcodeProduct.Kind { + init(_ kind: XcodeProjectRepresentation.Target.Product.Kind) { + switch kind { + case .application: + self = .application + case .executable: + self = .executable + case .framework: + self = .framework + case .library: + self = .library + case .other(let ident): + self = .other(ident) + } + } +} diff --git a/Sources/SPMBuildCore/Plugins/PluginInvocation.swift b/Sources/SPMBuildCore/Plugins/PluginInvocation.swift index 877e8443ad5..a3210f7e6ea 100644 --- a/Sources/SPMBuildCore/Plugins/PluginInvocation.swift +++ b/Sources/SPMBuildCore/Plugins/PluginInvocation.swift @@ -30,7 +30,14 @@ public enum PluginAction { pluginGeneratedSources: [AbsolutePath], pluginGeneratedResources: [AbsolutePath] ) + case createXcodeProjectBuildToolCommands( + project: XcodeProjectRepresentation, + target: XcodeProjectRepresentation.Target, + pluginGeneratedSources: [AbsolutePath], + pluginGeneratedResources: [AbsolutePath] + ) case performCommand(package: ResolvedPackage, arguments: [String]) + case performXcodeProjectCommand(project: XcodeProjectRepresentation, arguments: [String]) } public struct PluginTool { @@ -172,6 +179,8 @@ extension PluginModule { targets: serializer.targets, products: serializer.products, packages: serializer.packages, + xcodeTargets: serializer.xcodeTargets, + xcodeProjects: serializer.xcodeProjects, pluginWorkDirId: pluginWorkDirId, toolSearchDirIds: toolSearchDirIds, accessibleTools: accessibleTools) @@ -182,6 +191,32 @@ extension PluginModule { pluginGeneratedSources: generatedSources, pluginGeneratedResources: generatedResources ) + + case .createXcodeProjectBuildToolCommands(let project, let target, let generatedSources, let generatedResources): + let rootProjectId = try serializer.serialize(xcodeProject: project) + guard let targetId = try serializer.serialize(xcodeTarget: target) else { + throw StringError("unexpectedly was unable to serialize target \(target)") + } + let pluginGeneratedSources = try generatedSources.map { try serializer.serialize(path: $0) } + let pluginGeneratedResources = try generatedResources.map { try serializer.serialize(path: $0) } + let wireInput = WireInput( + paths: serializer.paths, + targets: serializer.targets, + products: serializer.products, + packages: serializer.packages, + xcodeTargets: serializer.xcodeTargets, + xcodeProjects: serializer.xcodeProjects, + pluginWorkDirId: pluginWorkDirId, + toolSearchDirIds: toolSearchDirIds, + accessibleTools: accessibleTools) + actionMessage = .createXcodeProjectBuildToolCommands( + context: wireInput, + rootProjectId: rootProjectId, + targetId: targetId, + pluginGeneratedSources: pluginGeneratedSources, + pluginGeneratedResources: pluginGeneratedResources + ) + case .performCommand(let package, let arguments): let rootPackageId = try serializer.serialize(package: package) let wireInput = WireInput( @@ -189,6 +224,8 @@ extension PluginModule { targets: serializer.targets, products: serializer.products, packages: serializer.packages, + xcodeTargets: serializer.xcodeTargets, + xcodeProjects: serializer.xcodeProjects, pluginWorkDirId: pluginWorkDirId, toolSearchDirIds: toolSearchDirIds, accessibleTools: accessibleTools) @@ -196,6 +233,23 @@ extension PluginModule { context: wireInput, rootPackageId: rootPackageId, arguments: arguments) + + case .performXcodeProjectCommand(let xcodeProject, let arguments): + let rootProjectId = try serializer.serialize(xcodeProject: xcodeProject) + let wireInput = WireInput( + paths: serializer.paths, + targets: serializer.targets, + products: serializer.products, + packages: serializer.packages, + xcodeTargets: serializer.xcodeTargets, + xcodeProjects: serializer.xcodeProjects, + pluginWorkDirId: pluginWorkDirId, + toolSearchDirIds: toolSearchDirIds, + accessibleTools: accessibleTools) + actionMessage = .performXcodeProjectCommand( + context: wireInput, + rootProjectId: rootProjectId, + arguments: arguments) } initialMessage = try actionMessage.toData() } diff --git a/Sources/SPMBuildCore/XcodeProjectRepresentation.swift b/Sources/SPMBuildCore/XcodeProjectRepresentation.swift new file mode 100644 index 00000000000..5fb066ac535 --- /dev/null +++ b/Sources/SPMBuildCore/XcodeProjectRepresentation.swift @@ -0,0 +1,74 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Basics + +/// Represents a simplifed view of an Xcode project to a plugin. +public struct XcodeProjectRepresentation: Equatable, Hashable { + public var displayName: String + public var directoryPath: AbsolutePath + public var filePaths: [AbsolutePath] + public var targets: [Target] + + public init(displayName: String, directoryPath: AbsolutePath, filePaths: [AbsolutePath], targets: [Target]) { + self.displayName = displayName + self.directoryPath = directoryPath + self.filePaths = filePaths + self.targets = targets + } + + public struct Target: Equatable, Hashable { + public var displayName: String + public var product: Product? + public var inputFiles: [InputFile] + + public init(displayName: String, product: Product?, inputFiles: [InputFile]) { + self.displayName = displayName + self.product = product + self.inputFiles = inputFiles + } + + public struct Product: Equatable, Hashable { + public var name: String + public var kind: Kind + + public init(name: String, kind: Kind) { + self.name = name + self.kind = kind + } + + /// Represents a kind of product produced by an Xcode target. + public enum Kind: Equatable, Hashable { + case application + case executable + case framework + case library + case other(String) + } + } + + public struct InputFile: Equatable, Hashable { + public var path: AbsolutePath + public var role: Role + + public init(path: AbsolutePath, role: Role) { + self.path = path + self.role = role + } + + public enum Role: Equatable, Hashable { + case source + case header + case resource + case unknown + } + } + } +} diff --git a/Sources/SwiftBuildSupport/PIF.swift b/Sources/SwiftBuildSupport/PIF.swift index 02d140f03b9..8d14a6a4b16 100644 --- a/Sources/SwiftBuildSupport/PIF.swift +++ b/Sources/SwiftBuildSupport/PIF.swift @@ -1119,6 +1119,11 @@ public enum PIF { /// Represents a filetype recognized by the Xcode build system. public struct SwiftBuildFileType: CaseIterable { + public static let xcassets: SwiftBuildFileType = SwiftBuildFileType( + fileType: "xcassets", + fileTypeIdentifier: "folder.abstractassetcatalog" + ) + public static let xcdatamodeld: SwiftBuildFileType = SwiftBuildFileType( fileType: "xcdatamodeld", fileTypeIdentifier: "wrapper.xcdatamodeld" diff --git a/Sources/SwiftBuildSupport/PIFBuilder.swift b/Sources/SwiftBuildSupport/PIFBuilder.swift index 2e0318ea7df..a873b69eedb 100644 --- a/Sources/SwiftBuildSupport/PIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PIFBuilder.swift @@ -931,6 +931,11 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder { pifTarget.addSourceFile(resourceFile) } + // Asset Catalogs need to be included in the sources target for generated asset symbols. + if SwiftBuildFileType.xcassets.fileTypes.contains(resource.path.extension ?? "") { + pifTarget.addSourceFile(resourceFile) + } + resourcesTarget.addResourceFile(resourceFile) } diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index f4d6d7d37cc..632f3e10d03 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -176,13 +176,19 @@ extension Workspace { missing: OrderedCollections.OrderedSet ) { - let manifestsMap: [PackageIdentity: Manifest] = try Dictionary( - throwingUniqueKeysWithValues: - root.packages.map { ($0.key, $0.value.manifest) } + - dependencies.map { - ($0.dependency.packageRef.identity, $0.manifest) - } - ) + // Temporary countermeasures against rdar://83316222; be robust against having colliding identities in both + // `root.packages` and `dependencies`. + var manifestsMap: [PackageIdentity: Manifest] = [:] + root.packages.map { ($0.key, $0.value.manifest) }.forEach { + if manifestsMap[$0.0] == nil { + manifestsMap[$0.0] = $0.1 + } + } + dependencies.map { ($0.dependency.packageRef.identity, $0.manifest) }.forEach { + if manifestsMap[$0.0] == nil { + manifestsMap[$0.0] = $0.1 + } + } var inputIdentities: OrderedCollections.OrderedSet = [] let inputNodes: [GraphLoadingNode] = try root.packages.map { identity, package in diff --git a/Sources/XCBuildSupport/PIF.swift b/Sources/XCBuildSupport/PIF.swift index 504a865c436..fcd30ea9b50 100644 --- a/Sources/XCBuildSupport/PIF.swift +++ b/Sources/XCBuildSupport/PIF.swift @@ -1119,6 +1119,11 @@ public enum PIF { /// Represents a filetype recognized by the Xcode build system. public struct XCBuildFileType: CaseIterable { + public static let xcassets: XCBuildFileType = XCBuildFileType( + fileType: "xcassets", + fileTypeIdentifier: "folder.abstractassetcatalog" + ) + public static let xcdatamodeld: XCBuildFileType = XCBuildFileType( fileType: "xcdatamodeld", fileTypeIdentifier: "wrapper.xcdatamodeld" diff --git a/Sources/XCBuildSupport/PIFBuilder.swift b/Sources/XCBuildSupport/PIFBuilder.swift index 6592902f970..93b88286edc 100644 --- a/Sources/XCBuildSupport/PIFBuilder.swift +++ b/Sources/XCBuildSupport/PIFBuilder.swift @@ -306,6 +306,7 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder { settings[.SWIFT_ACTIVE_COMPILATION_CONDITIONS] = ["$(inherited)", "SWIFT_PACKAGE"] settings[.GCC_PREPROCESSOR_DEFINITIONS] = ["$(inherited)", "SWIFT_PACKAGE"] settings[.CLANG_ENABLE_OBJC_ARC] = "YES" + settings[.KEEP_PRIVATE_EXTERNS] = "NO" // We currently deliberately do not support Swift ObjC interface headers. settings[.SWIFT_INSTALL_OBJC_HEADER] = "NO" @@ -924,6 +925,11 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder { pifTarget.addSourceFile(resourceFile) } + // Asset Catalogs need to be included in the sources target for generated asset symbols. + if XCBuildFileType.xcassets.fileTypes.contains(resource.path.extension ?? "") { + pifTarget.addSourceFile(resourceFile) + } + resourcesTarget.addResourceFile(resourceFile) } diff --git a/Tests/BasicsTests/CancellatorTests.swift b/Tests/BasicsTests/CancellatorTests.swift index 98f73e319c3..e67030b0561 100644 --- a/Tests/BasicsTests/CancellatorTests.swift +++ b/Tests/BasicsTests/CancellatorTests.swift @@ -209,9 +209,14 @@ final class CancellatorTests: XCTestCase { XCTAssertNotNil(registrationKey) let finishSemaphore = DispatchSemaphore(value: 0) + DispatchQueue.sharedConcurrent.async { defer { finishSemaphore.signal() } - process.launch() + do { + try process.run() + } catch { + XCTFail("Process failed to run with error: \(error)") + } process.waitUntilExit() print("process finished") XCTAssertEqual(process.terminationStatus, SIGINT) @@ -235,6 +240,7 @@ final class CancellatorTests: XCTestCase { func testFoundationProcessForceKill() throws { #if os(macOS) + try withTemporaryDirectory { temporaryDirectory in let scriptPath = temporaryDirectory.appending("script") try localFileSystem.writeFileContents( @@ -280,9 +286,14 @@ final class CancellatorTests: XCTestCase { XCTAssertNotNil(registrationKey) let finishSemaphore = DispatchSemaphore(value: 0) + DispatchQueue.sharedConcurrent.async { defer { finishSemaphore.signal() } - process.launch() + do { + try process.run() + } catch { + XCTFail("Process failed to run with error: \(error)") + } process.waitUntilExit() print("process finished") XCTAssertEqual(process.terminationStatus, SIGTERM) @@ -305,6 +316,9 @@ final class CancellatorTests: XCTestCase { } func testConcurrency() throws { +#if !os(macOS) + try XCTSkipIf(true, "skipping on non-macOS because of timeout problems") +#endif let observability = ObservabilitySystem.makeForTesting() let cancellator = Cancellator(observabilityScope: observability.topScope) diff --git a/Tests/BasicsTests/PhonyTest.swift b/Tests/BasicsTests/PhonyTest.swift new file mode 100644 index 00000000000..5d7a0467bef --- /dev/null +++ b/Tests/BasicsTests/PhonyTest.swift @@ -0,0 +1,7 @@ +import Testing + +// This test exists to force xunit to report at least one test pass +// for systems that require the output to report something. +struct PhonyTest { + @Test func phonyPass() {} +} diff --git a/Tests/PackageLoadingTests/PDAppleProductLoadingTests.swift b/Tests/PackageLoadingTests/PDAppleProductLoadingTests.swift new file mode 100644 index 00000000000..3b918859da4 --- /dev/null +++ b/Tests/PackageLoadingTests/PDAppleProductLoadingTests.swift @@ -0,0 +1,107 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import XCTest + +import Basics +import TSCUtility +import _InternalTestSupport +import PackageModel +import PackageLoading + +class PackageDescriptionAppleProductLoadingTests: PackageDescriptionLoadingTests { + override var toolsVersion: ToolsVersion { + .vNext + } + + func testApplicationProducts() throws { + #if ENABLE_APPLE_PRODUCT_TYPES + let content = """ + import PackageDescription + import AppleProductTypes + let package = Package( + name: "Foo", + products: [ + .iOSApplication( + name: "Foo", + targets: ["Foo"], + bundleIdentifier: "com.my.app", + teamIdentifier: "ZXYTEAM123", + displayVersion: "1.4.2 Extra Cool", + bundleVersion: "1.4.2", + appIcon: .asset("icon"), + accentColor: .asset("accentColor"), + supportedDeviceFamilies: [.pad, .mac], + supportedInterfaceOrientations: [.portrait, .portraitUpsideDown(), .landscapeRight(.when(deviceFamilies: [.mac]))], + capabilities: [ + .camera(purposeString: "All the better to see you with…"), + .microphone(purposeString: "All the better to hear you with…", .when(deviceFamilies: [.pad, .phone])), + .localNetwork(purposeString: "Communication is key…", bonjourServiceTypes: ["_ipp._tcp"], .when(deviceFamilies: [.mac])) + ], + appCategory: .developerTools + ), + ], + targets: [ + .executableTarget( + name: "Foo" + ), + ] + ) + """ + + let observability = ObservabilitySystem.makeForTesting() + let (manifest, _) = try loadAndValidateManifest(content, observabilityScope: observability.topScope) + XCTAssertNoDiagnostics(observability.diagnostics) + + // Check the targets. We expect to have a single executable target. + XCTAssertEqual(manifest.targets.count, 1) + let mainTarget = manifest.targets[0] + XCTAssertEqual(mainTarget.type, .executable) + XCTAssertEqual(mainTarget.name, "Foo") + + // Check the products. We expect to have a single executable product with iOS-specific settings. + XCTAssertEqual(manifest.products.count, 1) + let appProduct = manifest.products[0] + + // Check the core properties and basic settings of the application product. + XCTAssertEqual(appProduct.type, .executable) + XCTAssertEqual(appProduct.settings.count, 5) + XCTAssertTrue(appProduct.settings.contains(.bundleIdentifier("com.my.app"))) + XCTAssertTrue(appProduct.settings.contains(.bundleVersion("1.4.2"))) + + // Find the "iOS Application Info" setting. + var appInfoSetting: ProductSetting.IOSAppInfo? = nil + for case let ProductSetting.iOSAppInfo(value) in appProduct.settings { + appInfoSetting = .init(value) + } + guard let appInfoSetting = appInfoSetting else { + return XCTFail("product has no .iOSAppInfo() setting") + } + + // Check the specific properties of the iOS Application Info. + XCTAssertEqual(appInfoSetting.appIcon, .asset(name: "icon")) + XCTAssertEqual(appInfoSetting.accentColor, .asset(name: "accentColor")) + XCTAssertEqual(appInfoSetting.supportedDeviceFamilies, [.pad, .mac]) + XCTAssertEqual(appInfoSetting.supportedInterfaceOrientations, [ + .portrait(condition: nil), + .portraitUpsideDown(condition: nil), + .landscapeRight(condition: .init(deviceFamilies: [.mac])) + ]) + XCTAssertEqual(appInfoSetting.capabilities, [ + .init(purpose: "camera", purposeString: "All the better to see you with…", condition: nil), + .init(purpose: "microphone", purposeString: "All the better to hear you with…", condition: .init(deviceFamilies: [.pad, .phone])), + .init(purpose: "localNetwork", purposeString: "Communication is key…", bonjourServiceTypes: ["_ipp._tcp"], condition: .init(deviceFamilies: [.mac])) + ]) + XCTAssertEqual(appInfoSetting.appCategory?.rawValue, "public.app-category.developer-tools") + #else + throw XCTSkip("ENABLE_APPLE_PRODUCT_TYPES is not set") + #endif + } +} diff --git a/Tests/PackageRegistryTests/RegistryClientTests.swift b/Tests/PackageRegistryTests/RegistryClientTests.swift index e9c824e812c..dc59f4daf12 100644 --- a/Tests/PackageRegistryTests/RegistryClientTests.swift +++ b/Tests/PackageRegistryTests/RegistryClientTests.swift @@ -1360,7 +1360,7 @@ final class RegistryClientTests: XCTestCase { version: version, customToolsVersion: nil, observabilityScope: ObservabilitySystem.NOOP, - callbackQueue: .sharedConcurrent, + callbackQueue: .sharedConcurrent ) { continuation.resume(with: $0) } } let parsedToolsVersion = try ToolsVersionParser.parse(utf8String: manifestSync) @@ -3419,7 +3419,7 @@ final class RegistryClientTests: XCTestCase { signatureFormat: .none, fileSystem: localFileSystem, observabilityScope: ObservabilitySystem.NOOP, - callbackQueue: .sharedConcurrent, + callbackQueue: .sharedConcurrent ) { result in continuation.resume(with: result) } } diff --git a/Tests/WorkspaceTests/ManifestSourceGenerationTests.swift b/Tests/WorkspaceTests/ManifestSourceGenerationTests.swift index a8ba631f65c..82d1f03db4d 100644 --- a/Tests/WorkspaceTests/ManifestSourceGenerationTests.swift +++ b/Tests/WorkspaceTests/ManifestSourceGenerationTests.swift @@ -549,6 +549,183 @@ final class ManifestSourceGenerationTests: XCTestCase { XCTAssertTrue(contents.contains(".library(name: \"Foo\", targets: [\"Bar\"], type: .static)"), "contents: \(contents)") } + /// Tests a fully customized iOSApplication (one that exercises every parameter in at least some way). + func testAppleProductSettings() throws { + #if ENABLE_APPLE_PRODUCT_TYPES + let manifestContents = """ + // swift-tools-version: 999.0 + import PackageDescription + let package = Package( + name: "Foo", + products: [ + .iOSApplication( + name: "Foo", + targets: ["Foo"], + bundleIdentifier: "com.my.app", + teamIdentifier: "ZXYTEAM123", + displayVersion: "1.4.2 Extra Cool", + bundleVersion: "1.4.2", + appIcon: .placeholder(icon: .cloud), + accentColor: .presetColor(.red), + supportedDeviceFamilies: [.phone, .pad, .mac], + supportedInterfaceOrientations: [ + .portrait, + .landscapeRight(), + .landscapeLeft(.when(deviceFamilies: [.mac])) + ], + capabilities: [ + .camera(purposeString: "All the better to see you with…", .when(deviceFamilies: [.pad, .phone])), + .fileAccess(.userSelectedFiles, mode: .readOnly, .when(deviceFamilies: [.mac])), + .fileAccess(.pictureFolder, mode: .readWrite, .when(deviceFamilies: [.mac])), + .fileAccess(.musicFolder, mode: .readOnly), + .fileAccess(.downloadsFolder, mode: .readWrite, .when(deviceFamilies: [.mac])), + .fileAccess(.moviesFolder, mode: .readWrite, .when(deviceFamilies: [.mac])), + .incomingNetworkConnections(.when(deviceFamilies: [.mac])), + .outgoingNetworkConnections(), + .microphone(purposeString: "All the better to hear you with…"), + .motion(purposeString: "Move along, move along, …"), + .localNetwork( + purposeString: "Communication is key…", + bonjourServiceTypes: ["_ipp._tcp", "_ipps._tcp"], + .when(deviceFamilies: [.mac]) + ), + .appTransportSecurity( + configuration: .init( + allowsArbitraryLoadsInWebContent: true, + allowsArbitraryLoadsForMedia: false, + allowsLocalNetworking: false, + exceptionDomains: [ + .init( + domainName: "not-shady-at-all-domain.biz", + includesSubdomains: true, + exceptionAllowsInsecureHTTPLoads: true, + exceptionMinimumTLSVersion: "2", + exceptionRequiresForwardSecrecy: false, + requiresCertificateTransparency: false + ) + ], + pinnedDomains: [ + .init( + domainName: "honest-harrys-pinned-domain.biz", + includesSubdomains : false, + pinnedCAIdentities : [["a": "b", "x": "y"], [:]], + pinnedLeafIdentities : [["v": "w"]] + ) + ] + ), + .when(deviceFamilies: [.phone, .pad]) + ) + ], + appCategory: .weather, + additionalInfoPlistContentFilePath: "some/path/to/a/file.plist" + ), + ], + targets: [ + .executableTarget( + name: "Foo" + ), + ] + ) + """ + try testManifestWritingRoundTrip(manifestContents: manifestContents, toolsVersion: .v5_5) + #else + throw XCTSkip("ENABLE_APPLE_PRODUCT_TYPES is not set") + #endif + } + + /// Tests loading an iOSApplication product configured with the `.asset(_)` variant of the + /// appIcon and accentColor parameters. + func testAssetBasedAccentColorAndAppIconAppleProductSettings() throws { + #if ENABLE_APPLE_PRODUCT_TYPES + let manifestContents = """ + // swift-tools-version: 999.0 + import PackageDescription + let package = Package( + name: "Foo", + products: [ + .iOSApplication( + name: "Foo", + targets: ["Foo"], + appIcon: .asset("AppIcon"), + accentColor: .asset("AccentColor") + ), + ], + targets: [ + .executableTarget( + name: "Foo" + ), + ] + ) + """ + try testManifestWritingRoundTrip(manifestContents: manifestContents, toolsVersion: .v5_5) + #else + throw XCTSkip("ENABLE_APPLE_PRODUCT_TYPES is not set") + #endif + } + + /// Tests loading an iOSApplication product configured with legacy 'iconAssetName' and 'accentColorAssetName' parameters. + func testLegacyAccentColorAndAppIconAppleProductSettings() throws { + #if ENABLE_APPLE_PRODUCT_TYPES + let manifestContents = """ + // swift-tools-version: 999.0 + import PackageDescription + let package = Package( + name: "Foo", + products: [ + .iOSApplication( + name: "Foo", + targets: ["Foo"], + iconAssetName: "icon", + accentColorAssetName: "accentColor" + ), + ], + targets: [ + .executableTarget( + name: "Foo" + ), + ] + ) + """ + try testManifestWritingRoundTrip(manifestContents: manifestContents, toolsVersion: .v5_5) + #else + throw XCTSkip("ENABLE_APPLE_PRODUCT_TYPES is not set") + #endif + } + + /// Tests the smallest allowed iOSApplication (one that has default values for everything not required). Make sure no defaults get added to it. + func testMinimalAppleProductSettings() throws { + #if ENABLE_APPLE_PRODUCT_TYPES + let manifestContents = """ + // swift-tools-version: 999.0 + import PackageDescription + let package = Package( + name: "Foo", + products: [ + .iOSApplication( + name: "Foo", + targets: ["Foo"], + accentColor: .asset("AccentColor"), + supportedDeviceFamilies: [ + .mac + ], + supportedInterfaceOrientations: [ + .portrait + ] + ), + ], + targets: [ + .executableTarget( + name: "Foo" + ), + ] + ) + """ + try testManifestWritingRoundTrip(manifestContents: manifestContents, toolsVersion: .v5_5) + #else + throw XCTSkip("ENABLE_APPLE_PRODUCT_TYPES is not set") + #endif + } + func testModuleAliasGeneration() async throws { let manifest = Manifest.createRootManifest( displayName: "thisPkg",