diff --git a/Sources/Build/BuildPlan.swift b/Sources/Build/BuildPlan.swift index ac908cda59f..c2528b4594b 100644 --- a/Sources/Build/BuildPlan.swift +++ b/Sources/Build/BuildPlan.swift @@ -1272,6 +1272,8 @@ public final class ProductBuildDescription { } case .plugin: throw InternalError("unexpectedly asked to generate linker arguments for a plugin product") + case .custom: + throw InternalError("unexpectedly asked to generate linker arguments for a custom product") } // Set rpath such that dynamic libraries are looked up @@ -1294,6 +1296,8 @@ public final class ProductBuildDescription { useStdlibRpath = true case .plugin: throw InternalError("unexpectedly asked to generate linker arguments for a plugin product") + case .custom: + throw InternalError("unexpectedly asked to generate linker arguments for a custom product") } if useStdlibRpath && buildParameters.triple.isDarwin() { @@ -1632,6 +1636,10 @@ public class BuildPlan { // for automatic libraries and plugins, because they don't produce any output. for product in graph.allProducts where product.type != .library(.automatic) && product.type != .plugin { + // Custom product types are currently ignored; in the long run they + // should be supported using product type plugins. + if case .custom(_, _) = product.type { continue } + // Determine the appropriate tools version to use for the product. // This can affect what flags to pass and other semantics. let toolsVersion = graph.package(for: product)?.manifest.toolsVersion ?? .v5_5 @@ -1807,7 +1815,7 @@ public class BuildPlan { switch product.type { case .library(.automatic), .library(.static), .plugin: return product.targets.map { .target($0, conditions: []) } - case .library(.dynamic), .test, .executable, .snippet: + case .library(.dynamic), .test, .executable, .snippet, .custom: return [] } } diff --git a/Sources/Build/LLBuildManifestBuilder.swift b/Sources/Build/LLBuildManifestBuilder.swift index cb6f2714135..fd0563225a8 100644 --- a/Sources/Build/LLBuildManifestBuilder.swift +++ b/Sources/Build/LLBuildManifestBuilder.swift @@ -593,6 +593,11 @@ extension LLBuildManifestBuilder { try addStaticTargetInputs(target) } + // Custom product types are currently ignored; in the long run + // they should be supported using product type plugins. + case .custom: + break + case .test: break } @@ -746,6 +751,12 @@ extension LLBuildManifestBuilder { for target in product.targets { addStaticTargetInputs(target) } + + // Custom product types are currently ignored; in the long run + // they should be supported using product type plugins. + case .custom: + break + case .test: break } @@ -919,6 +930,8 @@ extension ResolvedProduct { return "\(name)-\(config).exe" case .plugin: throw InternalError("unexpectedly asked for the llbuild target name of a plugin product") + case .custom: + throw InternalError("unexpectedly asked for the llbuild target name of a custom product") } } diff --git a/Sources/PackageCollections/Providers/JSONPackageCollectionProvider.swift b/Sources/PackageCollections/Providers/JSONPackageCollectionProvider.swift index 5ea5acb7424..0784a948c6a 100644 --- a/Sources/PackageCollections/Providers/JSONPackageCollectionProvider.swift +++ b/Sources/PackageCollections/Providers/JSONPackageCollectionProvider.swift @@ -410,6 +410,8 @@ extension PackageModel.ProductType { self = .snippet case .test: self = .test + case .custom(let typeName, let propertyData): + self = .custom(typeName, propertyData) } } } diff --git a/Sources/PackageCollectionsModel/PackageCollectionModel+v1.swift b/Sources/PackageCollectionsModel/PackageCollectionModel+v1.swift index b6e35e7aade..1c2de7a8134 100644 --- a/Sources/PackageCollectionsModel/PackageCollectionModel+v1.swift +++ b/Sources/PackageCollectionsModel/PackageCollectionModel+v1.swift @@ -333,12 +333,15 @@ extension PackageCollectionModel.V1 { /// A test product. case test + + /// A custom type (the properties are encoded in opaque data). + case custom(_ typeName: String, _ propertyData: Data) } } extension PackageCollectionModel.V1.ProductType: Codable { private enum CodingKeys: String, CodingKey { - case library, executable, plugin, snippet, test + case library, executable, plugin, snippet, test, custom } public func encode(to encoder: Encoder) throws { @@ -355,6 +358,10 @@ extension PackageCollectionModel.V1.ProductType: Codable { try container.encodeNil(forKey: .snippet) case .test: try container.encodeNil(forKey: .test) + case .custom(let a1, let a2): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .custom) + try unkeyedContainer.encode(a1) + try unkeyedContainer.encode(a2) } } @@ -376,6 +383,11 @@ extension PackageCollectionModel.V1.ProductType: Codable { self = .snippet case .test: self = .test + case .custom: + var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key) + let a1 = try unkeyedValues.decode(String.self) + let a2 = try unkeyedValues.decode(Data.self) + self = .custom(a1, a2) } } } diff --git a/Sources/PackageDescription/Product.swift b/Sources/PackageDescription/Product.swift index 83ebd374387..d3421e29540 100644 --- a/Sources/PackageDescription/Product.swift +++ b/Sources/PackageDescription/Product.swift @@ -8,6 +8,8 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors */ +import Foundation + /// The object that defines a package product. /// /// A package product defines an externally visible build artifact that's @@ -52,26 +54,18 @@ /// ]) /// ] /// ) -public class Product: Encodable { - private enum ProductCodingKeys: String, CodingKey { - case name - case type = "product_type" - } - - /// The name of the package product. +open class Product: Encodable { + /// The name of the product. public let name: String init(name: String) { self.name = name } - /// The executable product of a Swift package. + /// A product that builds an executable binary (such as a command line tool). public final class Executable: Product { - private enum ExecutableCodingKeys: CodingKey { - case targets - } - - /// The names of the targets in this product. + /// The names of the targets that comprise the executable product. + /// There must be exactly one `executableTarget` among them. public let targets: [String] init(name: String, targets: [String]) { @@ -81,60 +75,57 @@ public class Product: Encodable { public override func encode(to encoder: Encoder) throws { try super.encode(to: encoder) - var productContainer = encoder.container(keyedBy: ProductCodingKeys.self) - try productContainer.encode("executable", forKey: .type) - var executableContainer = encoder.container(keyedBy: ExecutableCodingKeys.self) - try executableContainer.encode(targets, forKey: .targets) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("executable", forKey: .type) + try container.encode(targets, forKey: .targets) } } - /// The library product of a Swift package. + /// A product that builds a library that other targets and products can link against. public final class Library: Product { - private enum LibraryCodingKeys: CodingKey { - case type - case targets - } + /// The names of the targets that comprise the library product. + public let targets: [String] /// The different types of a library product. public enum LibraryType: String, Encodable { - /// A statically linked library. + /// A statically linked library (its code will be incorporated + /// into clients that link to it). case `static` - /// A dynamically linked library. + /// A dynamically linked library (its code will be referenced + /// by clients that link to it). case `dynamic` } - /// The names of the targets in this product. - public let targets: [String] - - /// The type of the library. + /// The type of library. /// /// If the type is unspecified, the Swift Package Manager automatically - /// chooses a type based on the client's preference. + /// chooses a type based on how the library is used by the client. public let type: LibraryType? init(name: String, type: LibraryType? = nil, targets: [String]) { - self.type = type self.targets = targets + self.type = type super.init(name: name) } public override func encode(to encoder: Encoder) throws { try super.encode(to: encoder) - var productContainer = encoder.container(keyedBy: ProductCodingKeys.self) - try productContainer.encode("library", forKey: .type) - var libraryContainer = encoder.container(keyedBy: LibraryCodingKeys.self) - try libraryContainer.encode(type, forKey: .type) - try libraryContainer.encode(targets, forKey: .targets) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("library", forKey: .type) + try container.encode(targets, forKey: .targets) + let encoder = JSONEncoder() + struct EncodedLibraryProperties: Encodable { + public let type: LibraryType? + } + let properties = EncodedLibraryProperties(type: self.type) + let encodedProperties = String(decoding: try encoder.encode(properties), as: UTF8.self) + try container.encode(encodedProperties, forKey: .encodedProperties) } } /// The plugin product of a Swift package. public final class Plugin: Product { - private enum PluginCodingKeys: CodingKey { - case targets - } - - /// The name of the plugin target to vend as a product. + /// The name of the plugin targets to vend as a product. public let targets: [String] init(name: String, targets: [String]) { @@ -144,13 +135,35 @@ public class Product: Encodable { public override func encode(to encoder: Encoder) throws { try super.encode(to: encoder) - var productContainer = encoder.container(keyedBy: ProductCodingKeys.self) - try productContainer.encode("plugin", forKey: .type) - var pluginContainer = encoder.container(keyedBy: PluginCodingKeys.self) - try pluginContainer.encode(targets, forKey: .targets) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("plugin", forKey: .type) + try container.encode(targets, forKey: .targets) } } + /// The name of the product type to encode. + private class var productTypeName: String { return "unknown" } + + /// The string representation of any additional product properties. By + /// storing these as a separate encoded blob, the properties can be a + /// private contract between PackageDescription and whatever client will + /// interprest them, without libSwiftPM needing to know the contents. + private var encodedProperties: String? { return .none } + + enum CodingKeys: String, CodingKey { + case type, name, targets, encodedProperties + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Self.productTypeName, forKey: .type) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(encodedProperties, forKey: .encodedProperties) + } +} + + +extension Product { /// Creates a library product to allow clients that declare a dependency on this package /// to use the package's functionality. /// @@ -197,9 +210,4 @@ public class Product: Encodable { ) -> Product { return Plugin(name: name, targets: targets) } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: ProductCodingKeys.self) - try container.encode(name, forKey: .name) - } } diff --git a/Sources/PackageLoading/Diagnostics.swift b/Sources/PackageLoading/Diagnostics.swift index 4501685c107..4ec244c913c 100644 --- a/Sources/PackageLoading/Diagnostics.swift +++ b/Sources/PackageLoading/Diagnostics.swift @@ -30,7 +30,7 @@ extension Basics.Diagnostic { switch product.type { case .library(.automatic): typeString = "" - case .executable, .snippet, .plugin, .test, + case .executable, .snippet, .plugin, .test, .custom, .library(.dynamic), .library(.static): typeString = " (\(product.type))" } diff --git a/Sources/PackageLoading/ManifestJSONParser.swift b/Sources/PackageLoading/ManifestJSONParser.swift index 5a311f4c3e0..59afc8af5df 100644 --- a/Sources/PackageLoading/ManifestJSONParser.swift +++ b/Sources/PackageLoading/ManifestJSONParser.swift @@ -433,34 +433,27 @@ extension SystemPackageProviderDescription { extension PackageModel.ProductType { fileprivate init(v4 json: JSON) throws { - let productType = try json.get(String.self, forKey: "product_type") + let productType = try json.get(String.self, forKey: "type") switch productType { case "executable": self = .executable case "library": - let libraryType: ProductType.LibraryType - - let libraryTypeString: String? = json.get("type") - switch libraryTypeString { - case "static"?: - libraryType = .static - case "dynamic"?: - libraryType = .dynamic - case nil: - libraryType = .automatic - default: - throw InternalError("invalid product type \(productType)") + // Since library is still a built-in type, unpack its properties. + struct EncodedLibraryProperties: Decodable { + public let type: LibraryType? } - - self = .library(libraryType) - + let encodedProperties = try json.get(String.self, forKey: "encodedProperties") + let properties = try JSONDecoder().decode(EncodedLibraryProperties.self, from: Data(encodedProperties.utf8)) + self = .library(properties.type ?? .automatic) + case "plugin": self = .plugin default: - throw InternalError("unexpected product type: \(json)") + let encodedProperties = try json.get(String.self, forKey: "encodedProperties") + self = .custom(productType, Data(encodedProperties.utf8)) } } } diff --git a/Sources/PackageLoading/PackageBuilder.swift b/Sources/PackageLoading/PackageBuilder.swift index 55d0b3deb85..77a551d189b 100644 --- a/Sources/PackageLoading/PackageBuilder.swift +++ b/Sources/PackageLoading/PackageBuilder.swift @@ -1247,6 +1247,11 @@ public final class PackageBuilder { guard self.validatePluginProduct(product, with: targets) else { continue } + case .custom: + guard self.validateCustomProduct(product, with: targets) else { + continue + } + break } append(Product(name: product.name, type: product.type, targets: targets)) @@ -1259,7 +1264,7 @@ public final class PackageBuilder { // for them. let explicitProductsTargets = Set(self.manifest.products.flatMap{ product -> [String] in switch product.type { - case .library, .plugin, .test: + case .library, .plugin, .test, .custom: return [] case .executable, .snippet: return product.targets @@ -1355,6 +1360,12 @@ public final class PackageBuilder { } return true } + + private func validateCustomProduct(_ product: ProductDescription, with targets: [Target]) -> Bool { + // At this point there are no built-in restrictions on custom products. + // Here is where we would add them. + return true + } } /// We create this structure after scanning the filesystem for potential targets. diff --git a/Sources/PackageModel/ManifestSourceGeneration.swift b/Sources/PackageModel/ManifestSourceGeneration.swift index 1c058d0afd9..186a10ccec3 100644 --- a/Sources/PackageModel/ManifestSourceGeneration.swift +++ b/Sources/PackageModel/ManifestSourceGeneration.swift @@ -209,6 +209,9 @@ fileprivate extension SourceCodeFragment { self.init(enum: "plugin", subnodes: params, multiline: true) case .test: self.init(enum: "test", subnodes: params, multiline: true) + case .custom(let customType, let propertyData): + // FIXME: Should we throw here instead? That would turn this from a `rethrows` to a `throws`. + self.init(enum: "custom", subnodes: params, multiline: true) } } } diff --git a/Sources/PackageModel/Product.swift b/Sources/PackageModel/Product.swift index 3d3612731a7..d38c2181f00 100644 --- a/Sources/PackageModel/Product.swift +++ b/Sources/PackageModel/Product.swift @@ -10,6 +10,7 @@ import TSCBasic import TSCUtility +import Foundation public class Product: Codable { /// The name of the product. @@ -87,6 +88,9 @@ public enum ProductType: Equatable, Hashable { /// A test product. case test + + /// A custom type (the properties are encoded in opaque data). + case custom(_ typeName: String, _ propertyData: Data) public var isLibrary: Bool { guard case .library = self else { return false } @@ -181,6 +185,8 @@ extension ProductType: CustomStringConvertible { } case .plugin: return "plugin" + case .custom(let customType, _): + return customType } } } @@ -200,7 +206,7 @@ extension ProductFilter: CustomStringConvertible { extension ProductType: Codable { private enum CodingKeys: String, CodingKey { - case library, executable, snippet, plugin, test + case library, executable, snippet, plugin, test, custom } public func encode(to encoder: Encoder) throws { @@ -217,6 +223,10 @@ extension ProductType: Codable { try container.encodeNil(forKey: .plugin) case .test: try container.encodeNil(forKey: .test) + case let .custom(a1, a2): + var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .custom) + try unkeyedContainer.encode(a1) + try unkeyedContainer.encode(a2) } } @@ -238,6 +248,11 @@ extension ProductType: Codable { self = .snippet case .plugin: self = .plugin + case .custom: + var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key) + let a1 = try unkeyedValues.decode(String.self) + let a2 = try unkeyedValues.decode(Data.self) + self = .custom(a1, a2) } } } diff --git a/Sources/SPMBuildCore/BuildParameters.swift b/Sources/SPMBuildCore/BuildParameters.swift index 6df687ac5e4..46add24b389 100644 --- a/Sources/SPMBuildCore/BuildParameters.swift +++ b/Sources/SPMBuildCore/BuildParameters.swift @@ -295,7 +295,7 @@ public struct BuildParameters: Encodable { return RelativePath("lib\(product.name)\(triple.staticLibraryExtension)") case .library(.dynamic): return RelativePath("\(triple.dynamicLibraryPrefix)\(product.name)\(triple.dynamicLibraryExtension)") - case .library(.automatic), .plugin: + case .library(.automatic), .plugin, .custom: fatalError() case .test: guard !triple.isWASI() else { diff --git a/Sources/XCBuildSupport/PIFBuilder.swift b/Sources/XCBuildSupport/PIFBuilder.swift index 037808dea84..6f8a91539a3 100644 --- a/Sources/XCBuildSupport/PIFBuilder.swift +++ b/Sources/XCBuildSupport/PIFBuilder.swift @@ -345,7 +345,7 @@ final class PackagePIFProjectBuilder: PIFProjectBuilder { addMainModuleTarget(for: product) case .library: addLibraryTarget(for: product) - case .plugin: + case .plugin, .custom: return } } @@ -1414,6 +1414,8 @@ extension ProductType { return .library case .plugin: return .plugin + case .custom: + return .library } } } diff --git a/Tests/WorkspaceTests/ManifestSourceGenerationTests.swift b/Tests/WorkspaceTests/ManifestSourceGenerationTests.swift index e5d98adfaf2..8f0d1d73ebe 100644 --- a/Tests/WorkspaceTests/ManifestSourceGenerationTests.swift +++ b/Tests/WorkspaceTests/ManifestSourceGenerationTests.swift @@ -425,4 +425,68 @@ class ManifestSourceGenerationTests: XCTestCase { // Check that we generated what we expected. XCTAssertTrue(contents.contains(".library(name: \"Foo\", targets: [\"Bar\"], type: .static)"), "contents: \(contents)") } + + func testCustomProductTypes() throws { + // Create a manifest containing a fictional custom product having encoded custom properties. + let manifest = Manifest( + displayName: "MyLibrary", + path: AbsolutePath("/tmp/MyLibrary/Package.swift"), + packageKind: .root(AbsolutePath("/tmp/MyLibrary")), + packageLocation: "/tmp/MyLibrary", + platforms: [], + toolsVersion: .v5_5, + products: [ + .init( + name: "SampleApp", + type: .custom("sample.app-type", Data("{\"identifier\":\"org.me.my-app\",\"version\":42,\"genre\":\"utility\"}".utf8)), + targets: [] + ) + ] + ) + + // Generate the manifest contents, specifying that that the SampleProductTypes module should be included, and providing a handler for our custom product type. + let contents = try manifest.generateManifestFileContents(additionalImportModuleNames: ["SampleProductTypes"], customProductTypeSourceGenerator: { + // Generate a SourceCodeFragement for an instance of our custom product type. + product in + + + guard case let .custom(customProductTypeName, encodedPropertyData) = product.type else { return nil } + XCTAssertEqual(customProductTypeName, "sample.app-type") + + // This is the struct that's encoded in the product properties. + struct SampleAppProperties: Codable { + var identifier: String + var version: Int + var genre: Genre + enum Genre: String, Codable { + case design + case game + case utility + } + } + + // Decode the product properties. + let decoder = JSONDecoder() + let properties = try decoder.decode(SampleAppProperties.self, from: encodedPropertyData) + + // Check that we got the expected values. + XCTAssertEqual(properties.identifier, "org.me.my-app") + XCTAssertEqual(properties.version, 42) + XCTAssertEqual(properties.genre, .utility) + + // Construct and return a SourceCodeFragment. + return SourceCodeFragment(enum: "SampleAppType", subnodes: [ + SourceCodeFragment(key: "identifier", string: properties.identifier), + SourceCodeFragment(key: "version", integer: properties.version), + SourceCodeFragment(key: "genre", enum: properties.genre.rawValue) + ], multiline: true) + }) + + // Check that we generated what we expected. + XCTAssertTrue(contents.contains("import SampleProductTypes"), "contents: \(contents)") + XCTAssertTrue(contents.contains(".SampleAppType("), "contents: \(contents)") + XCTAssertTrue(contents.contains("identifier: \"org.me.my-app\""), "contents: \(contents)") + XCTAssertTrue(contents.contains("version: 42"), "contents: \(contents)") + XCTAssertTrue(contents.contains("genre: .utility"), "contents: \(contents)") + } }