Skip to content

WIP: Loosen the internal assumptions around product types, letting libSwiftPM clients provide additional ones #3765

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Sources/Build/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 []
}
}
Expand Down
13 changes: 13 additions & 0 deletions Sources/Build/LLBuildManifestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@ extension PackageModel.ProductType {
self = .snippet
case .test:
self = .test
case .custom(let typeName, let propertyData):
self = .custom(typeName, propertyData)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand All @@ -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)
}
}
}
Expand Down
106 changes: 57 additions & 49 deletions Sources/PackageDescription/Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]) {
Expand All @@ -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]) {
Expand All @@ -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.
///
Expand Down Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion Sources/PackageLoading/Diagnostics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))"
}
Expand Down
27 changes: 10 additions & 17 deletions Sources/PackageLoading/ManifestJSONParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Expand Down
13 changes: 12 additions & 1 deletion Sources/PackageLoading/PackageBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions Sources/PackageModel/ManifestSourceGeneration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
Loading