-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Package Manager Allow Targets to Depend on Products in the Same Package #2234
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
stackotter
wants to merge
4
commits into
swiftlang:main
Choose a base branch
from
stackotter:wip-swiftpm-same-package-product-dependencies
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
561cc9d
Initial SwiftPM 'enable targets to depend on products in the same pac…
stackotter 79f4228
Remove reference to a practical example because it felt out of place
stackotter 40afeb9
Move motivation detail to after the example package manifest
stackotter 817b346
Fix formatting, remove scaffolding paragraph, and add link to WIP imp…
stackotter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
145 changes: 145 additions & 0 deletions
145
proposals/NNNN-swiftpm-targets-same-package-product-dependencies.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
# Package Manager Allow Targets to Depend on Products in the Same Package | ||
|
||
* Proposal: [SE-NNNN](NNNN-swiftpm-same-package-product-dependencies.md) | ||
* Authors: [stackotter](https://github.com/stackotter), [Tammo Freese](https://github.com/tammofreese) | ||
* Review Manager: TBD | ||
* Status: **Awaiting implementation** | ||
* Implementation: [apple/swift-package-manager#7331](https://github.com/apple/swift-package-manager/pull/7331) | ||
|
||
## Introduction | ||
|
||
This proposal allows targets to depend on products within the same package; solving issues faced by packages vending multiple dynamic library products (such as code duplication and type casting related issues). | ||
|
||
Swift-evolution thread: [discussion thread](https://forums.swift.org/t/pitch-swiftpm-allow-targets-to-depend-on-products-in-the-same-package/57717) | ||
|
||
## Motivation | ||
|
||
Consider a package `Library` with the following package manifest: | ||
|
||
```swift | ||
// Library/Package.swift | ||
// swift-tools-version: 5.9 | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "Library", | ||
products: [ | ||
.library(name: "API", type: .dynamic, targets: ["API"]), | ||
.library(name: "Auth", type: .dynamic, targets: ["Auth"]), | ||
], | ||
targets: [ | ||
.target(name: "API", dependencies: ["Auth"]), | ||
.target(name: "Auth"), | ||
] | ||
) | ||
``` | ||
|
||
A consumer of `Library` may import both `LibAPI` and `LibAuth` leading to duplication of the `Auth` target in the final bundle of products (due to static linking between `API` and `Auth`). This increases code-size, and more importantly, breaks type casting in many scenarios (see [the example](#type-casting-example)). | ||
|
||
### Existing workarounds | ||
|
||
The current workaround for these issues is to separate the leaf product (i.e. `LibAuth`) into a separate package (often in a subdirectory of the root package) to allow dynamic linking between the offending targets/products (i.e. `API` would now depend on `LibAuth`). This has its own issues; it complicates what could be a very simple project structure, and it hides `LibAuth` from consumers of `Library` (because SwiftPM doesn't support vending multiple packages from a single repository, see [Rust's Cargo Workspaces](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html)). In order for users of `LibAPI` to be able to use `LibAuth`, `LibAuth` would have to be imported with `@_exported import LibAuth` in `API` (or in a dedicated re-exporting target). Alternatively, the two packages can be placed in separate repositories, however that often doesn't make sense for closely tied targets (e.g. an API client and its authentication implementation), and can hurt maintainability. | ||
|
||
### Another motivating use-case (plugin systems) | ||
|
||
Another motivating use-case is building plugin systems for tools/apps built with SwiftPM. The plugin API must be imported by both the app and the plugin, which requires dynamic linking between the app and the plugin API ([in order for type casting to work](#type-casting-example)), and means that the plugin API cannot be in the same package as the app. For the same reasons as above, this is unwieldy. The proposed changes would mean that such tools/apps would be able to be developed as a single package (which [greatly improves maintainability](https://forums.swift.org/t/pitch-swiftpm-allow-targets-to-depend-on-products-in-the-same-package/57717/12)). | ||
|
||
This use-case may become increasingly common as people start making cross-platform apps with Swift (using SwiftPM instead of Xcode projects). | ||
|
||
### Type casting example | ||
|
||
This example demonstrates the type casting issue mentioned above. The full demo is hosted in [the `type-casting-issue-demo` repository](https://github.com/stackotter/type-casting-issue-demo) so that readers can see the issue in action. | ||
|
||
When reading the following code, developers would expect that `isGitHubAccessToken(githubAccessToken)` is true, but it turns out that isn't! This is due to `LibAuth` and `LibAPI` being dynamic library products — changing them to static library products (and performing a clean build) restores the assumption. | ||
|
||
```swift | ||
// Library/Sources/Auth/Auth.swift | ||
public protocol AccessToken { /* ... */ } | ||
|
||
public struct MicrosoftAccessToken: AccessToken { /* ... */ } | ||
|
||
public struct GitHubAccessToken: AccessToken { /* ... */ } | ||
|
||
// Library/Sources/API/API.swift | ||
import Auth | ||
|
||
public func isGitHubAccessToken(_ accessToken: any AccessToken) -> Bool { | ||
return accessToken is GitHubAccessToken | ||
} | ||
|
||
// App/Sources/main.swift | ||
import LibAPI | ||
import LibAuth | ||
|
||
let microsoftToken = MicrosoftAccessToken(/* ... */) | ||
let githubToken = GitHubAccessToken(/* ... */) | ||
|
||
print("microsoftToken is GitHubAccessToken == ", microsoftToken is GitHubAccessToken) | ||
print("isGitHubAccessToken(microsoftToken) == ", isGitHubAccessToken(microsoftToken)) | ||
print("githubToken is GitHubAccessToken == ", githubToken is GitHubAccessToken) | ||
print("isGitHubAccessToken(githubToken) == ", isGitHubAccessToken(githubToken)) | ||
|
||
// Output: | ||
// > microsoftToken is GitHubAccessToken == false | ||
// > isGitHubAccessToken(microsoftToken) == false | ||
// > githubToken is GitHubAccessToken == true | ||
// > isGitHubAccessToken(githubToken) == false | ||
``` | ||
|
||
## Proposed solution | ||
|
||
The proposed solution is to introduce a new method `Target.Dependency.product(name:condition:)` so that targets can depend on products in the same package. This enables dynamic linking between code within the same package fixing both code-size concerns and type-casting issues. | ||
|
||
## Detailed design | ||
|
||
A new `innerProductItem(name:condition:)` case will be added to `Target.Dependency` as the underlying representation for same-package product dependencies. It will be accompanied by a `product(name:condition:)` method — just as all existing enum cases of `Target.Dependency` have accompanying methods. | ||
|
||
```swift | ||
extension Target { | ||
public enum Dependency { | ||
/// A dependency on a product in the same package. | ||
/// | ||
/// - Parameters: | ||
/// - name: The name of the product. | ||
/// - condition: A condition that limits the application of the target dependency. For example, only apply a dependency for a specific platform. | ||
case innerProductItem(name: String, condition: TargetDependencyCondition?) | ||
} | ||
} | ||
|
||
extension Target.Dependency { | ||
/// Creates a dependency on a product from the same package. | ||
/// | ||
/// - Parameters: | ||
/// - name: The name of the product. | ||
/// - condition: A condition that limits the application of the target dependency. For example, only apply a | ||
/// dependency for a specific platform. | ||
/// - Returns: A `Target.Dependency` instance. | ||
public static func product( | ||
name: String, | ||
condition: TargetDependencyCondition? = nil | ||
) -> Target.Dependency { | ||
return .innerProductItem(name: name, condition: condition) | ||
} | ||
} | ||
``` | ||
|
||
SwiftPM's package graph and package builder will be updated to accomodate the new dependency type — the changes required are relatively self-contained. | ||
|
||
The existing `Target.Dependency.product(name:package:moduleAliases:condition)` method will not be able to be used to depend on products in the current package as that would cause the meaning of the package manifest to change depending on the name of the package's root directory now that a package's name is no longer tied to the package manifest's `name` field. | ||
|
||
Similarly, products within the same package as a target will still be ignored when evaluating the target's by-name dependencies (otherwise this proposal would be introducing a breaking change). | ||
|
||
## Impact on existing packages | ||
|
||
This isn't a breaking change for users of SwiftPM, and won't affect existing packages. | ||
|
||
## Alternatives considered | ||
|
||
### Rust's Cargo Workspaces | ||
|
||
Rust's Cargo package manager has a feature called [Cargo Workspaces](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) which allows multiple packages to be vended from a single repository. This is very useful for keeping subsystems of a large project separate, including being able to keep their dependencies separate. SwiftPM could introduce a similar system which would make the existing workaround much more manageable, by essentially making it a supported use-case. However this would be a massive undertaking and would likely require much stronger motivation to be worthwhile. Additionally, this can already be somewhat achieved using [a custom Swift package registry implementation designed to vend subdirectories of a repository as separate packages](https://github.com/stackotter/swiftpm-workspaces), however that has issues of its own for local development (the separate packages can't depend on eachother via paths which means that this solution is basically useless). | ||
|
||
### Doing nothing | ||
|
||
There is already a workaround, so one option would be to do nothing, but the workaround is sufficiently inconvenient and often requires use of the unofficial `@_exported` attribute. This was enough for multiple people to vocally want a better solution. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.