Building on recent proposals to improve the expressivity of package manifests, I'd like to share a proposal to unify SwiftPM's concept of products and targets. This is a major change to SwiftPM's build model, so I'm sharing it a little earlier than I would otherwise to collect feedback on the overall design. There is the beginning of an implementation available, but right now it's quite minimal and just enough to explore some of the design ideas in the proposal. I'm looking forward to your feedback! I'm especially interested in hearing people's opinions on the proposed method for configuring target visibility, and how packages migrate to the new model.
One of the common product use-case for XCFramework vendors is to declare dependencies between binary targets using a wrapper target while exposing product with the same binary target name. One example showed here: SwiftPM - Binary target with sub-dependencies - #7 by bielikb
After products deprecation how can this be achieved?
It would remain possible for a target to declare a dependency on binaryTargets, and then that target could declare public visibility rather than exposing it via a product.
I strongly agree that targets and products are currently very confusing, and I am glad to see the SwiftPM team is already making effort toward resolving that.
But I think the confusion might stem from a more fundamental tension: SwiftPM package manifests currently serve a dual role as build recipes and artifact BOMs. While it’s beneficial to be able to derive a BOM from a build recipe, the pitch here seems to indicate that building and packaging really are separate steps, and just because I use SwiftPM for both doesn’t mean that a single primitive is going to be sufficiently expressive, forward-compatible, or understandable.
In other words, is the right call perhaps to lean more heavily into the split between products and targets? In the same context in which we are discussing declarative manifests, it seems reasonable for such a manifest to only declare its products, especially if it is a binary-only package.
I support the unification of products and targets.
I noticed the proposal include this
In general, existing CLI options for working with products will continue to be supported to allow working with packages that have older tools versions. For example, both --target and --product will continue to be supported by swift build.
This is great that we will continue to support them. However, I have a comment unrelated to the overall design.
SE-0509 added support for generating an SBOM. Currently, build time SBOM generation emits a failure when used with the --target argument.
❯ swift build --target Test --sbom-spec cyclonedx
Building for debugging...
/Users/bkhouri/Downloads/can_be_deleted/_radar/157839834/Test/Package.swift: Test-product: ld: warning: building for macOS-12.0, but linking with dylib '@rpath/AppleConnectClient.framework/Versions/A/AppleConnectClient' which was built for newer version 13.0
[68 / 70] Test-product
Build complete! (17.43 secs.)
error: --sbom-spec or SWIFTPM_BUILD_SBOM_SPEC cannot be used with --target flag
Although this proposal mentioned CLI changes, it does mention an update to swift package generate-sbom but does not explicitly mention the case of a build-time SBOM generation.
Can the proposal be updated explicitly indicating swift build --target <name> --sbom-spec <spec> will produce a SBOM if the target has a visibility set (.package or .public), preserving a similar behaviour as if the target was defined as a product?
This is a very welcome change to the package manifest. I know that in particular the plugin tool dependency can be a source of confusion because executable targets are automatically promoted as products, and depending on the circumstances the plugin might depend on one or the other, and the plugin code needs to use the artifact's name to work correctly.
One thing that the redundancy between products and targets affords is the ability to separate between the name of the artifact (e.g. libfoo, or some-exec), which is currently set on the product, and the name of the module (e.g. CFoo, SomeExecutable) set on the target. Swift has its conventional CamelCase and C* for wrappers of C code, which makes sense for the module identity. But, then target artifacts can be in namespaces expecting ke-bab, or other case systems for the file names.
Should there be a way to specify the name for the artifact, separate from the module name for a target, public or not, in cases where it matters?
I think this is something we could consider allowing, but it would need to be conditioned on the target type because in some cases the module name is expected to match the binary or wrapper. For example, on Apple platforms, there's an assumption that if a framework named Foo.framework contains a Swift module, that module's name is Foo, and the compiler relies on this to avoid expensive directory traversals when looking for modules in framework search paths. A CLI tool has no such requirement, so there's an argument to be made the module name should be freely customizable there.
I wonder specifically about C targets with a module map where the convention is to name them CXyz, meanwhile, the library should probably be [lib]xyz.[so|dylib] because that's what the library name the C world might expect for it.
Currently SwiftPM adds the prefix/extension based on the targeted platform, so a dynamic library product Foo, for example, would produce libFoo.so when targeting Linux. Right now the proposal doesn't suggest changing that, but it's something we could consider
As a user I think the old Product type approach has better readability when working on Package.swift file. The problem with the new visibility parameter approach is that I have to read through the entire file to look for targets with public visibility to know what the package exports. I wonder if you have considered putting public targets and package-internal targets in two separate arrays? (That would also remove the visibility parameter from target public API).
// swift-tools-version:<version that ships this proposal>
import PackageDescription
let package = Package(
name: "FrameworkC",
platforms: [
.iOS(.v13)
],
targets: [
.binaryTarget(
name: "FrameworkC",
url: "url-to-framework-c",
checksum: "checksum"
),
.binaryTarget(
name: "FrameworkA",
url: "url-to-framework-a",
checksum: "checksum"
),
// The stub target is still required — `binaryTarget` gains no
// `dependencies:` parameter in this proposal. It is now exposed by
// its own visibility instead of by a library product.
// But imports will break if FrameworkCTargets name is kept.
// FrameworkC name won't be allowed because binaryTarget already
// uses this name.
.libraryTarget(
name: "FrameworkCTargets",
dependencies: [
.target(name: "FrameworkA", condition: .when(platforms: [.iOS])),
.target(name: "FrameworkC", condition: .when(platforms: [.iOS]))
],
path: "FrameworkCTargets",
visibility: .public
)
],
swiftLanguageModes: [.v5]
)
Wouldn't this break imports since users who are using import FrameworkC would have to use import FrameworkCTargets?
I am all in favor of simplifying and/or cleaning up the current package manifest, I don't have strong opinions on anything specific, but the pitch reads reasonably to me.
However, I'd like to mention that I have always found "target" to be a super unfortunate and confusing term. I understand the history of it, and how it is kind of established in "build system lingo", but I always found it an untidy choice as the central package manifest primitive.
I understand if we don't want to touch it, but since this pitch feels like a once-in-a-decade disruption I am asking: Can we throw alternative naming schemes into the mix as well and liberate SwiftPM from the "target" terminology entirely?
module, build unit, component, package member, buildable, package item, .... just something that does not collide with both target and target.
Internally, can we rename Module back to Target? Only some Targets are modules as we keep adding more over time. I just added class CustomTarget: Module which is very confusing but I refuse to call it a module because it's not .
I know "target" is way overused, but for build systems, it has commonly been used to specify the thing to be built, which is how we're using it here. The compiler uses "target" to specify the target machine for the object file it produces. But we are where we are. Since this is not really a new concept in SwiftPM (in fact products came after), I think the name is fine and should be familiar to developers.
The increasing complexity of this initializer is becoming a bit of a problem, and I think we should start thinking about ways to address it, but I think it's something we should consider separately. I don't think either improvement has a dependency on the other, and I don't think this pitch introduces anything which complicates future evolution.
This pitch is indeed proposing a disruptive change, but I'm very hesitant to use that as an excuse to make similarly disruptive changes in other areas without a very strong motivation. It's important that migration to the new tools version is as easy as possible for existing packages, and even mechanical renames make the process just a little bit more complicated. I'm not convinced any of the proposed alternatives in this thread are 'better enough', especially considering 'target' is a well-established concept in most build systems.
Overall, while the API naming for new concepts in this pitch like "visibility" is important to get right, I'm trying to focus primarily on changes that improve manifest expressivity over changes which improve manifest syntax.
I'm generally in favor of unifying those and getting rid of the confusion around products and targets. I'm curious to understand what we think the future of bare .target is if we introduce .libraryTarget. In the swift-driver example there are still a bunch of .target usages after the migration. What do we need to get rid of those?
Is there a reason that these are named .libraryTarget, .executableTarget, etc and not just .library, .executable, etc? Like @sliemeobn I’ve always been confused by the term target, and the constant repetition of it isn’t really making things easier to understand.
Bare .target remains useful for a target which wants to build a module without linking a final image. We could spell that as .libraryTarget(type: .object) if we wanted. IMO the consistency benefit isn't worth it given it would be a mechanical change for every package with no functional benefit. Similarly, .libraryTarget is named for consistency with .executableTarget and I don't think renaming executableTarget and forcing a migration is worth the benefits.